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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +151 -1134
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +69 -1249
  6. data/benchmark/baseline.json +2 -3
  7. data/benchmark/bench_agent_invoke.rb +4 -4
  8. data/benchmark/bench_context_assembler.rb +134 -34
  9. data/benchmark/bench_regression.rb +26 -6
  10. data/benchmark/bench_tool_schema.rb +2 -35
  11. data/docs/changelog/0.14-and-earlier.md +1137 -0
  12. data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
  13. data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
  14. data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
  15. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
  16. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  17. data/docs/features.md +87 -0
  18. data/docs/getting-started.md +351 -0
  19. data/docs/migrations/0.15.md +35 -0
  20. data/docs/migrations/0.16.md +43 -0
  21. data/docs/runtime-and-concurrency.md +258 -0
  22. data/examples/workflows/generic_task_event_mapping.rb +14 -6
  23. data/lib/phronomy/agent/agent_invocation.rb +2 -36
  24. data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
  25. data/lib/phronomy/agent/agent_root.rb +1 -2
  26. data/lib/phronomy/agent/async_event_api.rb +3 -3
  27. data/lib/phronomy/agent/base.rb +170 -333
  28. data/lib/phronomy/agent/context/capability/base.rb +177 -298
  29. data/lib/phronomy/agent/context_assembler.rb +65 -29
  30. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
  31. data/lib/phronomy/agent/context_plan_validator.rb +0 -33
  32. data/lib/phronomy/agent/execution_coordinator.rb +6 -7
  33. data/lib/phronomy/agent/journal_projection.rb +28 -2
  34. data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
  35. data/lib/phronomy/agent/shared_state.rb +48 -138
  36. data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
  37. data/lib/phronomy/agent/tool_executor.rb +29 -71
  38. data/lib/phronomy/agent/tool_invocation.rb +180 -336
  39. data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
  40. data/lib/phronomy/agent.rb +6 -10
  41. data/lib/phronomy/configuration.rb +4 -171
  42. data/lib/phronomy/diagnostics.rb +12 -41
  43. data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
  44. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
  45. data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
  46. data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
  47. data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
  48. data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
  49. data/lib/phronomy/engine/event_loop.rb +89 -190
  50. data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
  51. data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
  52. data/lib/phronomy/engine/runtime.rb +49 -377
  53. data/lib/phronomy/engine/task.rb +136 -277
  54. data/lib/phronomy/llm_adapter/base.rb +14 -14
  55. data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
  56. data/lib/phronomy/llm_adapter.rb +2 -2
  57. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  58. data/lib/phronomy/metrics.rb +15 -30
  59. data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
  60. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
  61. data/lib/phronomy/multi_agent/handoff.rb +1 -0
  62. data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
  63. data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
  64. data/lib/phronomy/testing/eval/comparison.rb +23 -0
  65. data/lib/phronomy/testing/eval/dataset.rb +27 -0
  66. data/lib/phronomy/testing/eval/eval_case.rb +13 -0
  67. data/lib/phronomy/testing/eval/eval_result.rb +16 -0
  68. data/lib/phronomy/testing/eval/metrics.rb +43 -0
  69. data/lib/phronomy/testing/eval/runner.rb +52 -0
  70. data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
  71. data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
  72. data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
  73. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
  74. data/lib/phronomy/testing/eval/scorer.rb +10 -0
  75. data/lib/phronomy/testing/eval.rb +9 -0
  76. data/lib/phronomy/testing/fake_clock.rb +6 -53
  77. data/lib/phronomy/testing.rb +2 -6
  78. data/lib/phronomy/tools/agent.rb +141 -6
  79. data/lib/phronomy/vector_store/async_backend.rb +21 -17
  80. data/lib/phronomy/vector_store/base.rb +2 -2
  81. data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
  82. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  83. data/lib/phronomy/version.rb +1 -1
  84. data/lib/phronomy/workflow_runner.rb +2 -4
  85. data/lib/phronomy.rb +7 -121
  86. data/scripts/api_snapshot.rb +4 -15
  87. metadata +24 -38
  88. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  89. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  90. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  91. data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
  92. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
  93. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
  94. data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
  95. data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
  96. data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
  97. data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
  98. data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
  99. data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
  100. data/lib/phronomy/engine/task/backend.rb +0 -80
  101. data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
  102. data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
  103. data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
  104. data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
  105. data/lib/phronomy/engine/task/thread_backend.rb +0 -84
  106. data/lib/phronomy/engine/task_group.rb +0 -265
  107. data/lib/phronomy/eval/comparison.rb +0 -47
  108. data/lib/phronomy/eval/dataset.rb +0 -45
  109. data/lib/phronomy/eval/eval_case.rb +0 -17
  110. data/lib/phronomy/eval/eval_result.rb +0 -29
  111. data/lib/phronomy/eval/metrics.rb +0 -66
  112. data/lib/phronomy/eval/runner.rb +0 -94
  113. data/lib/phronomy/eval/scorer/base.rb +0 -22
  114. data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
  115. data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
  116. data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
  117. data/lib/phronomy/eval/scorer.rb +0 -9
  118. data/lib/phronomy/eval.rb +0 -7
  119. data/lib/phronomy/knowledge_source.rb +0 -12
  120. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  121. data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
  122. data/lib/phronomy/testing/fake_scheduler.rb +0 -104
  123. 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 {BlockingAdapterPool} instances.
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 [BlockingAdapterPool]
24
+ # @return [OffloadPool]
25
25
  # @api private
26
26
  def default_pool(pool_size: 10, queue_size: 100)
27
- @default ||= BlockingAdapterPool.new(
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 [BlockingAdapterPool]
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] ||= BlockingAdapterPool.new(
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
- private_constant :STOP
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
- @task = @runtime.__spawn_event_loop_service { run_loop }
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
- completion_queue =
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
- completion_queue
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 do
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
- Phronomy::Task.current.equal?(@task)
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 do
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
- def shutdown(deadline:, cancel_grace:)
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
- begin_draining
231
- if wait_until_idle(deadline)
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
- if task_alive?
238
- cancel_and_cleanup(cancel_grace)
239
- elsif state == :failed
240
- :failed
241
- else
242
- finalize_terminated(:terminated)
243
- end
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
- def task_alive?
248
- @task&.alive? || false
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
- item = dequeue
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
- rescue Phronomy::CancellationError => error
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 do
284
- @idle_cond.broadcast
285
- end
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
- complete_waiter(
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
- enqueue(STOP)
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 do |waiter|
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
- @waiting.values.each do |waiter|
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
- begin
457
- @task&.join(remaining)
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
- @task = nil unless @task&.alive?
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
- if payload.is_a?(Exception)
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
- "depth=#{depth} max_depth=#{max_depth} " \
545
- "threshold=#{QUEUE_BACKLOG_WARNING_THRESHOLD} " \
546
- "event=#{event.type.inspect} " \
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
- "in queue (threshold: #{threshold}s)"
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
- "on the EventLoop thread (threshold: #{threshold}s). " \
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