phronomy 0.20.0 → 0.22.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 +3 -1
- data/CHANGELOG.md +41 -11
- data/CONTRIBUTING.md +44 -6
- data/README.md +26 -6
- data/docs/decisions/010-cooperative-first-concurrency.md +86 -69
- data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +184 -0
- data/docs/features.md +22 -13
- data/docs/getting-started.md +36 -18
- data/docs/runtime-and-concurrency.md +110 -147
- data/examples/workflows/agent_event_mapping.rb +27 -30
- data/lib/phronomy/agent/async_event_api.rb +14 -4
- data/lib/phronomy/agent/tool_executor.rb +7 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +142 -245
- data/lib/phronomy/engine/task.rb +50 -6
- data/lib/phronomy/invocation_context.rb +11 -52
- data/lib/phronomy/llm_adapter/base.rb +29 -32
- data/lib/phronomy/llm_adapter/ruby_llm.rb +13 -12
- data/lib/phronomy/llm_adapter.rb +10 -7
- data/lib/phronomy/output_parser/base.rb +5 -1
- data/lib/phronomy/tool/base.rb +15 -0
- data/lib/phronomy/tool.rb +11 -0
- data/lib/phronomy/vector_store/async_backend.rb +15 -38
- data/lib/phronomy/vector_store/base.rb +12 -13
- data/lib/phronomy/vector_store/embeddings/base.rb +11 -9
- data/lib/phronomy/version.rb +1 -1
- data/scripts/run_mutation.sh +2 -1
- data/sig/phronomy/agent.rbs +36 -0
- data/sig/phronomy/extensions.rbs +50 -0
- data/sig/phronomy/llm_adapter.rbs +11 -0
- data/sig/phronomy/persistence.rbs +70 -0
- data/sig/phronomy/runtime.rbs +43 -0
- data/sig/phronomy/tool.rbs +39 -0
- data/sig/phronomy/workflow.rbs +30 -0
- data/sig/phronomy.rbs +49 -1
- metadata +12 -2
|
@@ -22,7 +22,7 @@ module Phronomy
|
|
|
22
22
|
#
|
|
23
23
|
# 1. The total number of worker OS threads is capped.
|
|
24
24
|
# 2. Queue depth is bounded (backpressure when the pool is saturated).
|
|
25
|
-
# 3. Per-operation timeouts and cancellation settle the caller-facing
|
|
25
|
+
# 3. Per-operation timeouts and cancellation settle the caller-facing Task.
|
|
26
26
|
# 4. Operations that settle after worker execution has started are tracked as
|
|
27
27
|
# abandoned until that worker returns.
|
|
28
28
|
# 5. Metrics expose active work, queue depth, cumulative abandonment,
|
|
@@ -33,128 +33,24 @@ module Phronomy
|
|
|
33
33
|
# resource isolation may use named pools via {Runtime#pool}.
|
|
34
34
|
#
|
|
35
35
|
# @example Submitting synchronous work
|
|
36
|
-
#
|
|
37
|
-
# result =
|
|
36
|
+
# task = runtime.offload.submit(timeout: 30) { expensive_call }
|
|
37
|
+
# result = task.wait_result
|
|
38
38
|
#
|
|
39
39
|
# @example With cancellation
|
|
40
40
|
# token = Phronomy::Concurrency::CancellationToken.timeout_after(60)
|
|
41
|
-
#
|
|
42
|
-
# result =
|
|
41
|
+
# task = pool.submit(timeout: 30, cancellation_token: token) { expensive_call }
|
|
42
|
+
# result = task.wait_result
|
|
43
43
|
class OffloadPool
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# @return [Boolean] true when the submit-time deadline settled the operation
|
|
56
|
-
# @api private
|
|
57
|
-
def timed_out?
|
|
58
|
-
@mutex.synchronize { @timed_out }
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# @return [Boolean] true when submit cancellation settled the operation
|
|
62
|
-
# @api private
|
|
63
|
-
def cancelled?
|
|
64
|
-
@mutex.synchronize { @cancelled }
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# @return [Boolean] true when timeout/cancellation settled the caller-facing
|
|
68
|
-
# operation after worker execution had started. The worker is not forcibly
|
|
69
|
-
# interrupted and its eventual result is discarded.
|
|
70
|
-
# @api private
|
|
71
|
-
def abandoned?
|
|
72
|
-
@mutex.synchronize { @abandoned }
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# @return [Float] seconds spent in the queue before execution started
|
|
76
|
-
# @api private
|
|
77
|
-
def wait_time
|
|
78
|
-
@wait_time || 0.0
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Blocks the calling thread until the operation settles and returns its value.
|
|
82
|
-
#
|
|
83
|
-
# A +timeout+ passed here is local to this synchronous waiter. When it expires,
|
|
84
|
-
# {Phronomy::TimeoutError} is raised to this caller, but the operation is not
|
|
85
|
-
# settled, marked abandoned, or otherwise changed. The worker continues, and
|
|
86
|
-
# another waiter or an +on_complete+ callback may receive the eventual result
|
|
87
|
-
# unless the submit-time deadline or submit cancellation settles the operation
|
|
88
|
-
# first.
|
|
89
|
-
#
|
|
90
|
-
# Operation-wide cancellation belongs exclusively to the
|
|
91
|
-
# +cancellation_token:+ passed to {OffloadPool#submit}. PendingOperation does
|
|
92
|
-
# not define a separate waiter-local cancellation-token lifecycle.
|
|
93
|
-
#
|
|
94
|
-
# @param timeout [Numeric, nil] maximum seconds this waiter will block
|
|
95
|
-
# @return [Object]
|
|
96
|
-
# @raise [Phronomy::TimeoutError]
|
|
97
|
-
# @raise [Exception] error that settled the submitted operation
|
|
98
|
-
# @api private
|
|
99
|
-
def blocking_wait(timeout: nil)
|
|
100
|
-
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
|
|
101
|
-
value, error = @mutex.synchronize do
|
|
102
|
-
until @done
|
|
103
|
-
if deadline
|
|
104
|
-
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
105
|
-
if remaining <= 0
|
|
106
|
-
raise Phronomy::TimeoutError,
|
|
107
|
-
"timed out waiting for offloaded operation after #{timeout}s"
|
|
108
|
-
end
|
|
109
|
-
@cond.wait(@mutex, remaining)
|
|
110
|
-
else
|
|
111
|
-
@cond.wait(@mutex)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
[@value, @error]
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
raise error if error
|
|
119
|
-
|
|
120
|
-
value
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Unified wait interface compatible with {Phronomy::Task#wait_result}.
|
|
124
|
-
alias_method :wait_result, :blocking_wait
|
|
125
|
-
|
|
126
|
-
# Registers an independent callback to be called when the operation settles.
|
|
127
|
-
#
|
|
128
|
-
# If the operation has already settled, the callback is invoked immediately
|
|
129
|
-
# on the calling thread. Otherwise it may be invoked on a pool worker thread,
|
|
130
|
-
# on the EventLoop thread when a timer fires, or on the thread that explicitly
|
|
131
|
-
# cancels the submit cancellation token. The execution thread is not
|
|
132
|
-
# guaranteed; callbacks must be thread-safe and should complete quickly.
|
|
133
|
-
#
|
|
134
|
-
# Completion callback failures are logged and isolated. One callback cannot
|
|
135
|
-
# suppress delivery to later callbacks or change the operation's settled
|
|
136
|
-
# result.
|
|
137
|
-
#
|
|
138
|
-
# The callback receives +result+ and +error+ (one of them will be +nil+).
|
|
139
|
-
#
|
|
140
|
-
# @yield [result, error]
|
|
141
|
-
# @return [self]
|
|
142
|
-
# @api private
|
|
143
|
-
def on_complete(&callback)
|
|
144
|
-
raise ArgumentError, "on_complete requires a block" unless callback
|
|
145
|
-
|
|
146
|
-
fire_args = nil
|
|
147
|
-
@mutex.synchronize do
|
|
148
|
-
if @done
|
|
149
|
-
fire_args = [@value, @error]
|
|
150
|
-
else
|
|
151
|
-
@callbacks ||= []
|
|
152
|
-
@callbacks << callback
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
deliver_completion_callback(callback, *fire_args) if fire_args
|
|
156
|
-
self
|
|
157
|
-
end
|
|
44
|
+
# Private execution record for one submitted synchronous operation.
|
|
45
|
+
#
|
|
46
|
+
# Caller-facing completion is represented exclusively by {Phronomy::Task}.
|
|
47
|
+
# This object owns only OffloadPool-specific execution state: queue timing,
|
|
48
|
+
# worker-start linearization, submit timeout/cancellation, abandonment, and
|
|
49
|
+
# the submitted block itself.
|
|
50
|
+
#
|
|
51
|
+
# @api private
|
|
52
|
+
class Operation
|
|
53
|
+
attr_reader :task
|
|
158
54
|
|
|
159
55
|
# @api private
|
|
160
56
|
def initialize(
|
|
@@ -162,15 +58,15 @@ module Phronomy
|
|
|
162
58
|
timeout: nil,
|
|
163
59
|
cancellation_token: nil,
|
|
164
60
|
on_abandoned: nil,
|
|
165
|
-
submitted_at: nil
|
|
61
|
+
submitted_at: nil,
|
|
62
|
+
task_name: nil
|
|
166
63
|
)
|
|
167
64
|
@block = block
|
|
168
65
|
@timeout = timeout
|
|
169
66
|
@cancellation_token = cancellation_token
|
|
170
67
|
@on_abandoned = on_abandoned
|
|
171
|
-
@
|
|
172
|
-
@
|
|
173
|
-
@done = false
|
|
68
|
+
@task = Phronomy::Task.deferred(name: task_name)
|
|
69
|
+
@settled = false
|
|
174
70
|
@timed_out = false
|
|
175
71
|
@cancelled = false
|
|
176
72
|
@started = false
|
|
@@ -179,7 +75,6 @@ module Phronomy
|
|
|
179
75
|
@submitted_at = submitted_at ||
|
|
180
76
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
181
77
|
@mutex = Mutex.new
|
|
182
|
-
@cond = ConditionVariable.new
|
|
183
78
|
|
|
184
79
|
# Explicit submit cancellation is operation-wide. Deadline-only tokens are
|
|
185
80
|
# promoted to cancel! by OffloadPool#submit using the Runtime timer queue.
|
|
@@ -189,32 +84,59 @@ module Phronomy
|
|
|
189
84
|
@cancellation_token&.on_cancel(&@cancellation_callback)
|
|
190
85
|
end
|
|
191
86
|
|
|
192
|
-
#
|
|
87
|
+
# @return [Boolean] true when caller-facing settlement has been claimed
|
|
88
|
+
# @api private
|
|
89
|
+
def settled?
|
|
90
|
+
@mutex.synchronize { @settled }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Boolean] true when the submit-time deadline settled the Task
|
|
94
|
+
# @api private
|
|
95
|
+
def timed_out?
|
|
96
|
+
@mutex.synchronize { @timed_out }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @return [Boolean] true when submit cancellation settled the Task
|
|
100
|
+
# @api private
|
|
101
|
+
def cancelled?
|
|
102
|
+
@mutex.synchronize { @cancelled }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @return [Boolean] true when timeout/cancellation settled the Task after
|
|
106
|
+
# worker execution had started. The worker is not forcibly interrupted.
|
|
107
|
+
# @api private
|
|
108
|
+
def abandoned?
|
|
109
|
+
@mutex.synchronize { @abandoned }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [Float] seconds spent in the queue before execution started
|
|
113
|
+
# @api private
|
|
114
|
+
def wait_time
|
|
115
|
+
@wait_time || 0.0
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Settles the caller-facing Task with a submit-time timeout.
|
|
193
119
|
#
|
|
194
120
|
# The worker is not interrupted. If execution has already started, the
|
|
195
121
|
# operation is marked abandoned and the worker's eventual result is discarded.
|
|
196
122
|
#
|
|
197
|
-
# @return [Boolean] true when this call
|
|
198
|
-
# operation had already settled
|
|
123
|
+
# @return [Boolean] true when this call won settlement
|
|
199
124
|
# @api private
|
|
200
125
|
def fire_timeout!
|
|
201
|
-
settle_early!(timed_out: true) do
|
|
126
|
+
settle_early!(timed_out: true, cancelled: false) do
|
|
202
127
|
Phronomy::TimeoutError.new(
|
|
203
128
|
"offloaded operation timed out after #{@timeout}s"
|
|
204
129
|
)
|
|
205
130
|
end
|
|
206
131
|
end
|
|
207
132
|
|
|
208
|
-
# Settles the
|
|
209
|
-
#
|
|
210
|
-
# Cancellation is caller-facing settlement, not asynchronous worker
|
|
211
|
-
# interruption. If execution has already started, the operation is marked
|
|
212
|
-
# abandoned and the worker continues until the synchronous call returns.
|
|
133
|
+
# Settles the caller-facing Task because its submit cancellation token was
|
|
134
|
+
# cancelled. Cancellation never injects Thread#raise into the worker.
|
|
213
135
|
#
|
|
214
|
-
# @return [Boolean] true when this call
|
|
136
|
+
# @return [Boolean] true when this call won settlement
|
|
215
137
|
# @api private
|
|
216
138
|
def fire_cancellation!
|
|
217
|
-
settle_early!(cancelled: true) do |started|
|
|
139
|
+
settle_early!(timed_out: false, cancelled: true) do |started|
|
|
218
140
|
message = if started
|
|
219
141
|
"offloaded operation cancelled during execution"
|
|
220
142
|
else
|
|
@@ -224,27 +146,22 @@ module Phronomy
|
|
|
224
146
|
end
|
|
225
147
|
end
|
|
226
148
|
|
|
227
|
-
# Marks an operation that could not be admitted to the pool as settled
|
|
228
|
-
# previously armed
|
|
149
|
+
# Marks an operation that could not be admitted to the pool as settled so
|
|
150
|
+
# previously armed timers become harmless no-ops.
|
|
229
151
|
#
|
|
230
152
|
# @param error [Exception, nil]
|
|
231
|
-
# @return [Boolean] true when this call
|
|
153
|
+
# @return [Boolean] true when this call won settlement
|
|
232
154
|
# @api private
|
|
233
155
|
def fail_submission!(error = nil)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
next false if @done
|
|
156
|
+
changed = claim_terminal!
|
|
157
|
+
return false unless changed
|
|
237
158
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
@
|
|
241
|
-
|
|
242
|
-
@
|
|
243
|
-
true
|
|
159
|
+
detach_submit_cancellation
|
|
160
|
+
if error
|
|
161
|
+
@task.fail(error)
|
|
162
|
+
else
|
|
163
|
+
@task.complete(nil)
|
|
244
164
|
end
|
|
245
|
-
|
|
246
|
-
detach_submit_cancellation if changed
|
|
247
|
-
deliver_completion_callbacks(callbacks, nil, error) if changed
|
|
248
165
|
changed
|
|
249
166
|
end
|
|
250
167
|
|
|
@@ -261,7 +178,7 @@ module Phronomy
|
|
|
261
178
|
end
|
|
262
179
|
|
|
263
180
|
should_run = @mutex.synchronize do
|
|
264
|
-
if @
|
|
181
|
+
if @settled
|
|
265
182
|
false
|
|
266
183
|
else
|
|
267
184
|
# Linearization point: after this assignment, a concurrent timeout or
|
|
@@ -281,7 +198,7 @@ module Phronomy
|
|
|
281
198
|
complete_with_value!(@block.call)
|
|
282
199
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
283
200
|
# Rescue all Exception subclasses so non-StandardError raises still
|
|
284
|
-
# settle the
|
|
201
|
+
# settle the Task and unblock waiters.
|
|
285
202
|
complete_with_error!(e)
|
|
286
203
|
raise if e.is_a?(SignalException) || e.is_a?(SystemExit)
|
|
287
204
|
end
|
|
@@ -289,38 +206,41 @@ module Phronomy
|
|
|
289
206
|
|
|
290
207
|
private
|
|
291
208
|
|
|
292
|
-
def settle_early!(timed_out
|
|
293
|
-
callbacks = nil
|
|
209
|
+
def settle_early!(timed_out:, cancelled:)
|
|
294
210
|
abandoned_now = false
|
|
295
211
|
error = nil
|
|
296
|
-
|
|
297
212
|
changed = @mutex.synchronize do
|
|
298
|
-
next false if @
|
|
213
|
+
next false if @settled
|
|
299
214
|
|
|
300
|
-
# The error and @started classification are decided under the same lock
|
|
301
|
-
# as settlement. This is the cancellation/worker-start linearization
|
|
302
|
-
# point: cancellation that wins here prevents execution; worker start
|
|
303
|
-
# that wins first produces an abandoned in-flight operation.
|
|
304
215
|
error = yield(@started)
|
|
305
|
-
@
|
|
216
|
+
@settled = true
|
|
306
217
|
@timed_out = timed_out
|
|
307
218
|
@cancelled = cancelled
|
|
308
|
-
@error = error
|
|
309
219
|
@abandoned = @started
|
|
310
220
|
abandoned_now = @abandoned
|
|
311
|
-
@cond.broadcast
|
|
312
|
-
callbacks = @callbacks
|
|
313
|
-
@callbacks = nil
|
|
314
221
|
true
|
|
315
222
|
end
|
|
316
223
|
return false unless changed
|
|
317
224
|
|
|
318
225
|
detach_submit_cancellation
|
|
319
226
|
notify_abandoned if abandoned_now
|
|
320
|
-
|
|
227
|
+
if cancelled
|
|
228
|
+
@task.cancel!(error)
|
|
229
|
+
else
|
|
230
|
+
@task.fail(error)
|
|
231
|
+
end
|
|
321
232
|
true
|
|
322
233
|
end
|
|
323
234
|
|
|
235
|
+
def claim_terminal!
|
|
236
|
+
@mutex.synchronize do
|
|
237
|
+
next false if @settled
|
|
238
|
+
|
|
239
|
+
@settled = true
|
|
240
|
+
true
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
324
244
|
def notify_abandoned
|
|
325
245
|
@on_abandoned&.call(self)
|
|
326
246
|
rescue => error
|
|
@@ -330,52 +250,21 @@ module Phronomy
|
|
|
330
250
|
end
|
|
331
251
|
|
|
332
252
|
def complete_with_value!(value)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
next false if @done
|
|
253
|
+
changed = claim_terminal!
|
|
254
|
+
return false unless changed
|
|
336
255
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
callbacks = @callbacks
|
|
341
|
-
@callbacks = nil
|
|
342
|
-
true
|
|
343
|
-
end
|
|
344
|
-
detach_submit_cancellation if changed
|
|
345
|
-
deliver_completion_callbacks(callbacks, value, nil) if changed
|
|
346
|
-
changed
|
|
256
|
+
detach_submit_cancellation
|
|
257
|
+
@task.complete(value)
|
|
258
|
+
true
|
|
347
259
|
end
|
|
348
260
|
|
|
349
261
|
def complete_with_error!(error)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
next false if @done
|
|
353
|
-
|
|
354
|
-
@error = error
|
|
355
|
-
@done = true
|
|
356
|
-
@cond.broadcast
|
|
357
|
-
callbacks = @callbacks
|
|
358
|
-
@callbacks = nil
|
|
359
|
-
true
|
|
360
|
-
end
|
|
361
|
-
detach_submit_cancellation if changed
|
|
362
|
-
deliver_completion_callbacks(callbacks, nil, error) if changed
|
|
363
|
-
changed
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
def deliver_completion_callbacks(callbacks, value, error)
|
|
367
|
-
callbacks&.each do |callback|
|
|
368
|
-
deliver_completion_callback(callback, value, error)
|
|
369
|
-
end
|
|
370
|
-
end
|
|
262
|
+
changed = claim_terminal!
|
|
263
|
+
return false unless changed
|
|
371
264
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
Phronomy.configuration.logger&.error do
|
|
376
|
-
"[OffloadPool::PendingOperation] on_complete callback raised " \
|
|
377
|
-
"#{callback_error.class}: #{callback_error.message}"
|
|
378
|
-
end
|
|
265
|
+
detach_submit_cancellation
|
|
266
|
+
@task.fail(error)
|
|
267
|
+
true
|
|
379
268
|
end
|
|
380
269
|
|
|
381
270
|
def detach_submit_cancellation
|
|
@@ -388,6 +277,7 @@ module Phronomy
|
|
|
388
277
|
@cancellation_callback = nil
|
|
389
278
|
end
|
|
390
279
|
end
|
|
280
|
+
private_constant :Operation
|
|
391
281
|
|
|
392
282
|
# @param pool_size [Integer] maximum number of worker threads
|
|
393
283
|
# @param queue_size [Integer] maximum pending operations waiting for a worker
|
|
@@ -422,24 +312,25 @@ module Phronomy
|
|
|
422
312
|
end
|
|
423
313
|
|
|
424
314
|
# Submits synchronous off-EventLoop work to the pool.
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
#
|
|
315
|
+
#
|
|
316
|
+
# Returns a {Phronomy::Task} immediately after queue admission; the block
|
|
317
|
+
# runs on a worker thread. Do not submit logical waits (for example waiting
|
|
318
|
+
# for a child Agent Task) merely to make them asynchronous; those belong to
|
|
428
319
|
# FSMSession/EventLoop completion events.
|
|
429
320
|
#
|
|
430
|
-
# A submit-time +timeout+ is an operation-wide deadline measured from the
|
|
431
|
-
# of this method, including queue wait. The timer settles the
|
|
432
|
-
#
|
|
433
|
-
#
|
|
434
|
-
# If it fires after execution starts, the
|
|
435
|
-
# eventual worker result is discarded.
|
|
321
|
+
# A submit-time +timeout+ is an operation-wide deadline measured from the
|
|
322
|
+
# start of this method, including queue wait. The timer settles the Task and
|
|
323
|
+
# notifies +on_complete+ without forcibly interrupting a running worker. If
|
|
324
|
+
# the deadline fires before worker execution starts, the block is skipped.
|
|
325
|
+
# If it fires after execution starts, the private Operation is marked
|
|
326
|
+
# abandoned and the eventual worker result is discarded.
|
|
436
327
|
#
|
|
437
|
-
# The submit +cancellation_token+ is also operation-wide. Explicit
|
|
438
|
-
# settles the
|
|
439
|
-
# attached to the Runtime timer queue so deadline expiry becomes
|
|
440
|
-
# cancellation without adding a polling Thread. Cancellation before
|
|
441
|
-
# skips the block; cancellation after execution starts abandons
|
|
442
|
-
# caller-facing result and never uses Thread#raise.
|
|
328
|
+
# The submit +cancellation_token+ is also operation-wide. Explicit
|
|
329
|
+
# cancellation settles the Task immediately. A token with a monotonic
|
|
330
|
+
# deadline is attached to the Runtime timer queue so deadline expiry becomes
|
|
331
|
+
# explicit cancellation without adding a polling Thread. Cancellation before
|
|
332
|
+
# execution skips the block; cancellation after execution starts abandons
|
|
333
|
+
# only the caller-facing result and never uses Thread#raise.
|
|
443
334
|
#
|
|
444
335
|
# Synchronous queue admission may delay return from this method when
|
|
445
336
|
# +on_full: :wait+ is used. EventLoop-owned framework paths therefore submit
|
|
@@ -450,7 +341,7 @@ module Phronomy
|
|
|
450
341
|
# @param on_full [Symbol] +:wait+, +:raise+, or +:timeout+
|
|
451
342
|
# @param full_timeout [Numeric, nil] queue-admission timeout for +on_full: :timeout+
|
|
452
343
|
# @yield block containing synchronous work
|
|
453
|
-
# @return [
|
|
344
|
+
# @return [Phronomy::Task]
|
|
454
345
|
# @raise [Phronomy::ConfigurationError] when a timer is required but no
|
|
455
346
|
# timer queue provider is configured
|
|
456
347
|
# @raise [Phronomy::PoolShutdownError] when the pool has been shut down
|
|
@@ -482,19 +373,21 @@ module Phronomy
|
|
|
482
373
|
"timer_queue is required when submit timeout or cancellation deadline is specified"
|
|
483
374
|
end
|
|
484
375
|
|
|
485
|
-
|
|
376
|
+
operation = Operation.new(
|
|
486
377
|
block,
|
|
487
378
|
timeout: timeout,
|
|
488
379
|
cancellation_token: cancellation_token,
|
|
489
380
|
submitted_at: submitted_at,
|
|
381
|
+
task_name: offload_task_name,
|
|
490
382
|
on_abandoned: method(:record_abandoned)
|
|
491
383
|
)
|
|
384
|
+
task = operation.task
|
|
492
385
|
|
|
493
386
|
# on_cancel only reacts to explicit cancel!, whereas cancelled? also covers a
|
|
494
387
|
# monotonic deadline. Promote an already-expired deadline immediately.
|
|
495
388
|
if already_cancelled
|
|
496
389
|
cancellation_token.cancel!
|
|
497
|
-
return
|
|
390
|
+
return task
|
|
498
391
|
end
|
|
499
392
|
|
|
500
393
|
begin
|
|
@@ -502,13 +395,13 @@ module Phronomy
|
|
|
502
395
|
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - submitted_at
|
|
503
396
|
remaining = timeout.to_f - elapsed
|
|
504
397
|
if remaining <= 0
|
|
505
|
-
|
|
506
|
-
return
|
|
398
|
+
operation.fire_timeout!
|
|
399
|
+
return task
|
|
507
400
|
end
|
|
508
401
|
|
|
509
402
|
# Arm before queue admission so the deadline includes time spent waiting
|
|
510
403
|
# for a queue slot.
|
|
511
|
-
timer_queue.schedule(seconds: remaining) {
|
|
404
|
+
timer_queue.schedule(seconds: remaining) { operation.fire_timeout! }
|
|
512
405
|
end
|
|
513
406
|
|
|
514
407
|
if cancellation_remaining
|
|
@@ -516,19 +409,19 @@ module Phronomy
|
|
|
516
409
|
remaining = cancellation_token.remaining_monotonic_seconds
|
|
517
410
|
if remaining <= 0
|
|
518
411
|
cancellation_token.cancel!
|
|
519
|
-
return
|
|
412
|
+
return task
|
|
520
413
|
end
|
|
521
414
|
timer_queue.schedule(seconds: remaining) { cancellation_token.cancel! }
|
|
522
415
|
end
|
|
523
416
|
|
|
524
417
|
# Cancellation/timeout can race with timer registration. Do not enqueue
|
|
525
418
|
# already-settled work when the race is observable here.
|
|
526
|
-
return
|
|
419
|
+
return task if operation.settled?
|
|
527
420
|
|
|
528
421
|
case on_full
|
|
529
422
|
when :raise
|
|
530
423
|
begin
|
|
531
|
-
@queue.push(
|
|
424
|
+
@queue.push(operation, true)
|
|
532
425
|
rescue ThreadError
|
|
533
426
|
raise Phronomy::BackpressureError,
|
|
534
427
|
"OffloadPool queue is full (depth: #{@queue_size})"
|
|
@@ -538,9 +431,9 @@ module Phronomy
|
|
|
538
431
|
(Process.clock_gettime(Process::CLOCK_MONOTONIC) + full_timeout) :
|
|
539
432
|
nil
|
|
540
433
|
loop do
|
|
541
|
-
return
|
|
434
|
+
return task if operation.settled?
|
|
542
435
|
|
|
543
|
-
@queue.push(
|
|
436
|
+
@queue.push(operation, true)
|
|
544
437
|
break
|
|
545
438
|
rescue ThreadError
|
|
546
439
|
if deadline &&
|
|
@@ -551,18 +444,18 @@ module Phronomy
|
|
|
551
444
|
sleep(0.005)
|
|
552
445
|
end
|
|
553
446
|
else # :wait (default)
|
|
554
|
-
@queue.push(
|
|
447
|
+
@queue.push(operation)
|
|
555
448
|
end
|
|
556
449
|
rescue ClosedQueueError => e
|
|
557
450
|
# Shutdown raced with this submit — preserve the existing public error.
|
|
558
|
-
|
|
451
|
+
operation.fail_submission!(e)
|
|
559
452
|
raise Phronomy::PoolShutdownError, "pool has been shut down"
|
|
560
453
|
rescue => e
|
|
561
|
-
|
|
454
|
+
operation.fail_submission!(e)
|
|
562
455
|
raise
|
|
563
456
|
end
|
|
564
457
|
|
|
565
|
-
|
|
458
|
+
task
|
|
566
459
|
end
|
|
567
460
|
|
|
568
461
|
# Gracefully drains the pool and terminates all worker threads.
|
|
@@ -635,20 +528,24 @@ module Phronomy
|
|
|
635
528
|
SENTINEL = :shutdown
|
|
636
529
|
private_constant :SENTINEL
|
|
637
530
|
|
|
531
|
+
def offload_task_name
|
|
532
|
+
@name ? "offload-#{@name}" : "offload"
|
|
533
|
+
end
|
|
534
|
+
|
|
638
535
|
def spawn_worker(index = nil)
|
|
639
536
|
label = ["phronomy", "offload-pool", @name, index].compact.join("-")
|
|
640
537
|
Thread.new do
|
|
641
538
|
Thread.current.name = label
|
|
642
539
|
loop do
|
|
643
|
-
|
|
540
|
+
operation = begin
|
|
644
541
|
@queue.pop
|
|
645
542
|
rescue ClosedQueueError
|
|
646
543
|
break
|
|
647
544
|
end
|
|
648
545
|
# nil is returned by a closed, empty Queue on some Ruby versions
|
|
649
|
-
break if
|
|
546
|
+
break if operation.nil? || operation == SENTINEL
|
|
650
547
|
|
|
651
|
-
run_operation(
|
|
548
|
+
run_operation(operation)
|
|
652
549
|
end
|
|
653
550
|
end
|
|
654
551
|
end
|
|
@@ -663,18 +560,18 @@ module Phronomy
|
|
|
663
560
|
end
|
|
664
561
|
end
|
|
665
562
|
|
|
666
|
-
def run_operation(
|
|
667
|
-
operation_id =
|
|
563
|
+
def run_operation(operation)
|
|
564
|
+
operation_id = operation.object_id
|
|
668
565
|
@mutex.synchronize do
|
|
669
566
|
@active_count += 1
|
|
670
567
|
@running_operation_ids[operation_id] = true
|
|
671
568
|
end
|
|
672
569
|
|
|
673
570
|
begin
|
|
674
|
-
|
|
571
|
+
operation.execute!
|
|
675
572
|
ensure
|
|
676
|
-
abandoned =
|
|
677
|
-
wait_ns = (
|
|
573
|
+
abandoned = operation.abandoned?
|
|
574
|
+
wait_ns = (operation.wait_time * 1_000_000_000).to_i
|
|
678
575
|
|
|
679
576
|
@mutex.synchronize do
|
|
680
577
|
@active_count -= 1
|