phronomy 0.13.0 → 0.14.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.
@@ -34,19 +34,27 @@ module Phronomy
34
34
  # result = op.blocking_wait
35
35
  class BlockingAdapterPool
36
36
  # Represents the pending result of a submitted blocking operation.
37
- # Returned immediately by {BlockingAdapterPool#submit}; call {#await} to
38
- # wait for the result.
37
+ # Returned immediately by {BlockingAdapterPool#submit}; call {#blocking_wait}
38
+ # to wait for the result.
39
39
  class PendingOperation
40
- # @return [Boolean] true when the operation has finished (success or error)
40
+ # @return [Boolean] true when the caller-facing result has settled
41
+ # (success, failure, cancellation, or submit-time timeout)
41
42
  # @api private
42
43
  def done?
43
44
  @mutex.synchronize { @done }
44
45
  end
45
46
 
46
- # @return [Boolean] true when the operation was abandoned due to timeout
47
+ # @return [Boolean] true when the submit-time deadline settled the operation
48
+ # @api private
49
+ def timed_out?
50
+ @mutex.synchronize { @timed_out }
51
+ end
52
+
53
+ # @return [Boolean] true when a submit-time timeout occurred after worker
54
+ # execution had started. The worker is not forcibly interrupted.
47
55
  # @api private
48
56
  def abandoned?
49
- @abandoned
57
+ @mutex.synchronize { @abandoned }
50
58
  end
51
59
 
52
60
  # @return [Float] seconds spent in the queue before execution started
@@ -57,45 +65,28 @@ module Phronomy
57
65
 
58
66
  # Blocks until the operation completes and returns its value.
59
67
  #
60
- # An optional +timeout+ (in seconds) may be passed here; it is measured
61
- # from the moment +await+ is called. If both a submit-time timeout and an
62
- # await-time timeout are present, the earlier deadline wins. The worker
63
- # thread is NOT interrupted it runs to completion on its own.
68
+ # A +timeout+ passed here is local to this waiter. When it expires,
69
+ # {Phronomy::TimeoutError} is raised to this caller, but the operation is not
70
+ # settled, marked abandoned, or otherwise changed. The worker continues, and
71
+ # another waiter or an +on_complete+ callback may receive the eventual result
72
+ # unless the submit-time deadline or cancellation settles the operation first.
73
+ #
74
+ # A submit-time timeout passed to {BlockingAdapterPool#submit} is enforced by
75
+ # the runtime timer queue independently of this method and is therefore not
76
+ # re-read here.
64
77
  #
65
78
  # An optional +cancellation_token+ may be passed here (or at submit time).
66
79
  # If the token is cancelled while waiting, {Phronomy::CancellationError} is
67
- # raised immediately without interrupting the worker.
80
+ # raised without interrupting the worker.
68
81
  #
69
82
  # **Cooperative path (`:fiber` / `DeterministicScheduler`):**
70
- # When called from a Fiber managed by {DeterministicScheduler} (i.e. under
71
- # the +:fiber+ runtime backend), the calling Fiber suspends cooperatively
72
- # via +Fiber.yield+ rather than blocking the OS thread. The Fiber is
73
- # resumed on the scheduler's ready queue once the worker thread completes
74
- # the operation.
83
+ # When called from a Fiber managed by {DeterministicScheduler}, the calling
84
+ # Fiber suspends cooperatively via +Fiber.yield+ rather than blocking the OS
85
+ # thread. The Fiber is resumed through +on_complete+ when the operation
86
+ # settles. A waiter-local +timeout:+ is not enforced on this path; use the
87
+ # submit-time timeout for an operation-wide deadline.
75
88
  #
76
- # @note **Cooperative cancellation semantics** (ADR-010):
77
- # Phronomy uses a non-preemptive, cooperative-first concurrency model.
78
- # Cancellation is *cooperative*, not preemptive:
79
- # - When a +cancellation_token+ is cancelled, +CancellationError+ is
80
- # raised to the +await+ caller immediately; when the timeout fires,
81
- # +TimeoutError+ is raised instead. In both cases, the underlying
82
- # worker thread is **not** forcibly stopped.
83
- # - The worker thread will complete its submitted block naturally.
84
- # Code inside the block must call +token.check!+ at suitable
85
- # checkpoints to observe the cancelled state and exit early.
86
- # - There is no +Thread#kill+ or +Thread#raise+ involved. The framework
87
- # never forcibly terminates worker threads.
88
- #
89
- # @note **Cooperative timeout limitation**: the +timeout:+ parameter passed
90
- # to +await+ is *not* enforced on the cooperative path. The calling Fiber
91
- # remains suspended until the worker thread finishes regardless of how many
92
- # seconds elapse. This is because the cooperative scheduler cannot
93
- # preempt a running OS thread. If a time bound is required, set
94
- # +timeout:+ at {BlockingAdapterPool#submit submit} time instead; the pool
95
- # will then abandon the operation on the worker side and mark it as
96
- # {#abandoned?}.
97
- #
98
- # @param timeout [Numeric, nil] seconds from now before raising TimeoutError
89
+ # @param timeout [Numeric, nil] maximum seconds this waiter will block
99
90
  # (thread path only; ignored on the cooperative/fiber path)
100
91
  # @param cancellation_token [CancellationToken, nil]
101
92
  # @return [Object]
@@ -104,86 +95,71 @@ module Phronomy
104
95
  # @raise [Exception] error raised inside the submitted block
105
96
  # @api private
106
97
  def blocking_wait(timeout: nil, cancellation_token: nil)
107
- effective_timeout = [timeout, @timeout].compact.min
108
98
  effective_token = cancellation_token || @cancellation_token
109
99
 
110
100
  raise CancellationError, "blocking operation cancelled" if effective_token&.cancelled?
111
101
 
112
102
  # Cooperative context: suspend the calling Fiber rather than blocking
113
103
  # the OS thread so that DeterministicScheduler can continue dispatching
114
- # other tasks while waiting for the blocking worker to finish.
115
- # (Issue #338, ADR-010 Rule 3)
116
- # Uses the same thread-local key as Task::FiberBackend::SCHEDULER_KEY
117
- # (:phronomy_deterministic_scheduler) to avoid a cross-file constant
118
- # dependency at load time.
104
+ # other tasks while waiting for the blocking worker or submit-time timer.
119
105
  scheduler = Thread.current.thread_variable_get(:phronomy_deterministic_scheduler)
120
106
  in_managed_fiber = !Fiber.respond_to?(:main) || Fiber.current != Fiber.main
121
107
  if scheduler && in_managed_fiber
122
- unless @done
123
- # Register this await with the scheduler so run_until_idle knows
124
- # not to exit until the worker thread completes (Issue #338).
108
+ unless done?
125
109
  scheduler.track_blocking_await
126
110
  waiting_fiber = Fiber.current
127
111
  on_complete do |_result, _error|
128
- # Decrement the counter and wake run_until_idle, then re-enqueue
129
- # the suspended Fiber for cooperative resumption.
130
112
  scheduler.complete_blocking_await
131
113
  scheduler.enqueue_fiber(-> { waiting_fiber.resume })
132
114
  end
133
115
  Fiber.yield(:cooperative_suspend)
134
116
  end
117
+
135
118
  raise CancellationError, "blocking operation cancelled" if effective_token&.cancelled?
136
- raise @error if @error
137
119
 
138
- return @value
120
+ value, error = @mutex.synchronize { [@value, @error] }
121
+ raise error if error
122
+
123
+ return value
139
124
  end
140
125
 
141
126
  # Wake up the waiting thread whenever the token is cancelled so we can
142
- # propagate cancellation without sleeping until the timeout expires.
127
+ # propagate cancellation without sleeping until the operation completes.
143
128
  effective_token&.on_cancel { @mutex.synchronize { @cond.broadcast } }
144
129
 
145
- if effective_timeout
146
- deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + effective_timeout
147
- @mutex.synchronize do
148
- until @done
149
- raise CancellationError, "blocking operation cancelled" if effective_token&.cancelled?
130
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
131
+ value, error = @mutex.synchronize do
132
+ until @done
133
+ raise CancellationError, "blocking operation cancelled" if effective_token&.cancelled?
150
134
 
135
+ if deadline
151
136
  remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
152
137
  if remaining <= 0
153
- # Guard against double-counting when await is called multiple times.
154
- unless @abandoned
155
- @abandoned = true
156
- @on_abandoned&.call
157
- end
158
- raise Phronomy::TimeoutError, "blocking operation timed out after #{effective_timeout}s"
138
+ raise Phronomy::TimeoutError, "timed out waiting for blocking operation after #{timeout}s"
159
139
  end
160
140
  @cond.wait(@mutex, remaining)
161
- end
162
- end
163
- else
164
- @mutex.synchronize do
165
- until @done
166
- raise CancellationError, "blocking operation cancelled" if effective_token&.cancelled?
167
-
141
+ else
168
142
  @cond.wait(@mutex)
169
143
  end
170
144
  end
145
+
146
+ [@value, @error]
171
147
  end
172
- raise @error if @error
173
148
 
174
- @value
149
+ raise error if error
150
+
151
+ value
175
152
  end
176
153
 
177
154
  # Unified wait interface compatible with {Phronomy::Task#wait_result}.
178
- # Delegates to {#blocking_wait} so that callers treating the return value
179
- # of {ToolExecutor.call_async} uniformly as +#wait_result+ work correctly
180
- # regardless of whether a Task or PendingOperation is returned.
181
155
  alias_method :wait_result, :blocking_wait
182
156
 
183
- # Registers a callback to be called when the operation finishes.
184
- # If the operation has already finished the callback is invoked immediately
185
- # on the calling thread. Otherwise it is invoked on the worker thread that
186
- # completes the operation.
157
+ # Registers a callback to be called when the operation settles.
158
+ #
159
+ # If the operation has already settled, the callback is invoked immediately
160
+ # on the calling thread. Otherwise it may be invoked on a pool worker thread
161
+ # or on the runtime timer thread. The execution thread is not guaranteed;
162
+ # callbacks must be thread-safe and should complete quickly.
187
163
  #
188
164
  # The callback receives +result+ and +error+ (one of them will be +nil+).
189
165
  #
@@ -205,7 +181,7 @@ module Phronomy
205
181
  end
206
182
 
207
183
  # @api private
208
- def initialize(block, timeout: nil, cancellation_token: nil, on_abandoned: nil)
184
+ def initialize(block, timeout: nil, cancellation_token: nil, on_abandoned: nil, submitted_at: nil)
209
185
  @block = block
210
186
  @timeout = timeout
211
187
  @cancellation_token = cancellation_token
@@ -213,35 +189,117 @@ module Phronomy
213
189
  @value = nil
214
190
  @error = nil
215
191
  @done = false
192
+ @timed_out = false
193
+ @started = false
216
194
  @abandoned = false
217
195
  @wait_time = nil
218
- @submitted_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
196
+ @submitted_at = submitted_at || Process.clock_gettime(Process::CLOCK_MONOTONIC)
219
197
  @mutex = Mutex.new
220
198
  @cond = ConditionVariable.new
221
199
  end
222
200
 
201
+ # Settles the operation with a submit-time timeout.
202
+ #
203
+ # The worker is not interrupted. If execution has already started, the
204
+ # operation is marked abandoned and the worker's eventual result is discarded.
205
+ #
206
+ # @return [Boolean] true when this call settled the operation, false when the
207
+ # operation had already settled
208
+ # @api private
209
+ def fire_timeout!
210
+ error = Phronomy::TimeoutError.new(
211
+ "blocking operation timed out after #{@timeout}s"
212
+ )
213
+ callbacks = nil
214
+ abandoned_now = false
215
+
216
+ @mutex.synchronize do
217
+ return false if @done
218
+
219
+ @done = true
220
+ @timed_out = true
221
+ @error = error
222
+ @abandoned = @started
223
+ abandoned_now = @abandoned
224
+ @cond.broadcast
225
+ callbacks = @callbacks
226
+ @callbacks = nil
227
+ end
228
+
229
+ # Internal bookkeeping is completed before user callbacks run. A metrics
230
+ # callback must never suppress delivery of TimeoutError to on_complete.
231
+ if abandoned_now
232
+ begin
233
+ @on_abandoned&.call
234
+ rescue => e
235
+ Phronomy.configuration.logger&.error {
236
+ "BlockingAdapterPool abandoned callback failed: #{e.class}: #{e.message}"
237
+ }
238
+ end
239
+ end
240
+
241
+ callbacks&.each { |callback| callback.call(nil, error) }
242
+ true
243
+ end
244
+
245
+ # Marks an operation that could not be admitted to the pool as settled, so a
246
+ # previously armed submit-time timer becomes a harmless no-op.
247
+ #
248
+ # @param error [Exception, nil]
249
+ # @return [Boolean] true when this call changed the state
250
+ # @api private
251
+ def fail_submission!(error = nil)
252
+ @mutex.synchronize do
253
+ return false if @done
254
+
255
+ @done = true
256
+ @error = error if error
257
+ @cond.broadcast
258
+ end
259
+ true
260
+ end
261
+
262
+ # Executes the operation on a pool worker.
223
263
  # @api private
224
264
  def execute!
225
265
  @wait_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @submitted_at
226
266
 
227
- if @cancellation_token&.cancelled?
228
- complete_with_error!(CancellationError.new("operation cancelled before execution"))
267
+ cancellation_error = nil
268
+ callbacks = nil
269
+ should_run = @mutex.synchronize do
270
+ if @done
271
+ false
272
+ elsif @cancellation_token&.cancelled?
273
+ cancellation_error = CancellationError.new("operation cancelled before execution")
274
+ @done = true
275
+ @error = cancellation_error
276
+ @cond.broadcast
277
+ callbacks = @callbacks
278
+ @callbacks = nil
279
+ false
280
+ else
281
+ # Linearization point: after this assignment, a concurrent timeout is
282
+ # classified as an in-flight abandonment and the block will run.
283
+ @started = true
284
+ true
285
+ end
286
+ end
287
+
288
+ if cancellation_error
289
+ callbacks&.each { |callback| callback.call(nil, cancellation_error) }
229
290
  return
230
291
  end
231
292
 
293
+ return unless should_run
294
+
232
295
  # Do NOT use Timeout.timeout here — it delivers an async Thread#raise
233
296
  # that can corrupt external library state (mutexes, C extensions, etc.).
234
- # Timeout enforcement is handled cooperatively in #await instead.
235
- # Each blocking library (Net::HTTP, pg, redis, etc.) should set its
236
- # own native connection/read timeouts.
297
+ # Each blocking library should set its own native connection/read timeout.
237
298
  begin
238
299
  complete_with_value!(@block.call)
239
300
  rescue Exception => e # rubocop:disable Lint/RescueException
240
- # Rescue all Exception subclasses (not just StandardError) so that
241
- # non-StandardError raises such as NotImplementedError (< ScriptError)
242
- # still complete the operation and unblock any waiting #await callers.
243
- # Without this, a ScriptError in a pool worker would leave the
244
- # PendingOperation permanently incomplete, causing #await to deadlock.
301
+ # Rescue all Exception subclasses so non-StandardError raises still
302
+ # settle the operation and unblock waiters.
245
303
  complete_with_error!(e)
246
304
  raise if e.is_a?(SignalException) || e.is_a?(SystemExit)
247
305
  end
@@ -250,27 +308,33 @@ module Phronomy
250
308
  private
251
309
 
252
310
  def complete_with_value!(value)
253
- cbs = nil
311
+ callbacks = nil
254
312
  @mutex.synchronize do
313
+ return false if @done
314
+
255
315
  @value = value
256
316
  @done = true
257
317
  @cond.broadcast
258
- cbs = @callbacks
318
+ callbacks = @callbacks
259
319
  @callbacks = nil
260
320
  end
261
- cbs&.each { |cb| cb.call(value, nil) }
321
+ callbacks&.each { |callback| callback.call(value, nil) }
322
+ true
262
323
  end
263
324
 
264
325
  def complete_with_error!(error)
265
- cbs = nil
326
+ callbacks = nil
266
327
  @mutex.synchronize do
328
+ return false if @done
329
+
267
330
  @error = error
268
331
  @done = true
269
332
  @cond.broadcast
270
- cbs = @callbacks
333
+ callbacks = @callbacks
271
334
  @callbacks = nil
272
335
  end
273
- cbs&.each { |cb| cb.call(nil, error) }
336
+ callbacks&.each { |callback| callback.call(nil, error) }
337
+ true
274
338
  end
275
339
  end
276
340
 
@@ -278,12 +342,15 @@ module Phronomy
278
342
  # @param queue_size [Integer] maximum pending operations waiting for a worker
279
343
  # @param name [String, Symbol, nil] optional pool name used in thread labels
280
344
  # @param logger [Logger, nil] optional logger for warnings
345
+ # @param timer_queue_provider [#call, nil] returns a TimerQueue-compatible
346
+ # object. Required when +submit(timeout:)+ is used.
281
347
  # @api private
282
- def initialize(pool_size: 10, queue_size: 100, name: nil, logger: nil)
348
+ def initialize(pool_size: 10, queue_size: 100, name: nil, logger: nil, timer_queue_provider: nil)
283
349
  @pool_size = pool_size
284
350
  @queue_size = queue_size
285
351
  @name = name
286
352
  @logger = logger
353
+ @timer_queue_provider = timer_queue_provider
287
354
  @queue = SizedQueue.new(queue_size)
288
355
  @active_count = 0
289
356
  @abandoned_count = 0
@@ -295,35 +362,73 @@ module Phronomy
295
362
  end
296
363
 
297
364
  # Submits a blocking operation to the pool.
298
- # Returns a {PendingOperation} immediately; the block runs on a worker thread.
365
+ # Returns a {PendingOperation} immediately after queue admission; the block runs
366
+ # on a worker thread.
299
367
  #
300
- # @note **Cooperative callers**: if you are running under the `:fiber` backend
301
- # (i.e. inside a {DeterministicScheduler} Fiber), set +timeout:+ here
302
- # rather than on {PendingOperation#await}. The await-time timeout is not
303
- # enforced on the cooperative path (the Fiber cannot preempt a running
304
- # worker thread). A submit-time timeout triggers on the worker side and
305
- # marks the operation {PendingOperation#abandoned? abandoned}, which
306
- # unblocks the waiting Fiber via the normal on-complete callback.
307
- # @param timeout [Numeric, nil] seconds before the operation is abandoned
368
+ # A submit-time +timeout+ is an operation-wide deadline measured from the start
369
+ # of this method, including queue wait. The timer settles the PendingOperation
370
+ # and notifies +on_complete+ without forcibly interrupting a running worker.
371
+ # If the deadline fires before worker execution starts, the block is skipped and
372
+ # the operation is not counted as abandoned. If it fires after execution starts,
373
+ # the operation is marked abandoned and the eventual worker result is discarded.
374
+ #
375
+ # Synchronous queue admission may still delay return from this method when
376
+ # +on_full: :wait+ is used; resolving that requires interruptible admission.
377
+ #
378
+ # @param timeout [Numeric, nil] operation-wide deadline in seconds
308
379
  # @param cancellation_token [CancellationToken, nil]
380
+ # @param on_full [Symbol] +:wait+, +:raise+, or +:timeout+
381
+ # @param full_timeout [Numeric, nil] queue-admission timeout for +on_full: :timeout+
309
382
  # @yield block containing the blocking call
310
383
  # @return [PendingOperation]
384
+ # @raise [Phronomy::ConfigurationError] when +timeout+ is specified without a
385
+ # timer queue provider
311
386
  # @raise [Phronomy::PoolShutdownError] when the pool has been shut down
312
387
  # @raise [Phronomy::BackpressureError] when +on_full: :raise+ and queue is full
313
- # @raise [Phronomy::TimeoutError] when +on_full: :timeout+ and wait exceeds +full_timeout+
388
+ # @raise [Phronomy::TimeoutError] when +on_full: :timeout+ exceeds +full_timeout+
314
389
  # @api private
315
390
  def submit(timeout: nil, cancellation_token: nil, on_full: :wait, full_timeout: nil, &block)
316
391
  raise Phronomy::PoolShutdownError, "pool has been shut down" if @shutdown
317
392
 
318
- op = PendingOperation.new(block, timeout: timeout, cancellation_token: cancellation_token,
319
- on_abandoned: timeout ? -> { @mutex.synchronize { @abandoned_count += 1 } } : nil)
393
+ submitted_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
394
+ timer_queue = nil
395
+ if timeout
396
+ timer_queue = @timer_queue_provider&.call
397
+ unless timer_queue
398
+ raise Phronomy::ConfigurationError,
399
+ "timer_queue is required when submit timeout is specified"
400
+ end
401
+ end
402
+
403
+ op = PendingOperation.new(
404
+ block,
405
+ timeout: timeout,
406
+ cancellation_token: cancellation_token,
407
+ submitted_at: submitted_at,
408
+ on_abandoned: timeout ? -> { @mutex.synchronize { @abandoned_count += 1 } } : nil
409
+ )
410
+
320
411
  begin
412
+ if timeout
413
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - submitted_at
414
+ remaining = timeout.to_f - elapsed
415
+ if remaining <= 0
416
+ op.fire_timeout!
417
+ return op
418
+ end
419
+
420
+ # Arm before queue admission so the deadline includes time spent waiting
421
+ # for a queue slot.
422
+ timer_queue.schedule(seconds: remaining) { op.fire_timeout! }
423
+ end
424
+
321
425
  case on_full
322
426
  when :raise
323
427
  begin
324
428
  @queue.push(op, true)
325
429
  rescue ThreadError
326
- raise Phronomy::BackpressureError, "BlockingAdapterPool queue is full (depth: #{@queue_size})"
430
+ raise Phronomy::BackpressureError,
431
+ "BlockingAdapterPool queue is full (depth: #{@queue_size})"
327
432
  end
328
433
  when :timeout
329
434
  deadline = full_timeout ? (Process.clock_gettime(Process::CLOCK_MONOTONIC) + full_timeout) : nil
@@ -332,17 +437,23 @@ module Phronomy
332
437
  break
333
438
  rescue ThreadError
334
439
  if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
335
- raise Phronomy::TimeoutError, "timed out waiting for a free slot in BlockingAdapterPool"
440
+ raise Phronomy::TimeoutError,
441
+ "timed out waiting for a free slot in BlockingAdapterPool"
336
442
  end
337
443
  sleep(0.005)
338
444
  end
339
445
  else # :wait (default)
340
446
  @queue.push(op)
341
447
  end
342
- rescue ClosedQueueError
343
- # Shutdown raced with this submit — treat as if @shutdown was already set.
448
+ rescue ClosedQueueError => e
449
+ # Shutdown raced with this submit — preserve the existing public error.
450
+ op.fail_submission!(e)
344
451
  raise Phronomy::PoolShutdownError, "pool has been shut down"
452
+ rescue => e
453
+ op.fail_submission!(e)
454
+ raise
345
455
  end
456
+
346
457
  op
347
458
  end
348
459
 
@@ -358,7 +469,7 @@ module Phronomy
358
469
  def shutdown(drain_timeout: 30)
359
470
  @shutdown = true
360
471
  @queue.close
361
- @workers.each { |t| t.join(drain_timeout) }
472
+ @workers.each { |thread| thread.join(drain_timeout) }
362
473
  self
363
474
  end
364
475
 
@@ -376,14 +487,15 @@ module Phronomy
376
487
  @queue.size
377
488
  end
378
489
 
379
- # @return [Integer] number of operations that were abandoned due to timeout
490
+ # @return [Integer] number of operations whose caller-facing timeout fired
491
+ # after worker execution had started
380
492
  # @api private
381
493
  def abandoned_count
382
494
  @mutex.synchronize { @abandoned_count }
383
495
  end
384
496
 
385
- # Average time (in seconds) that completed operations spent in the queue
386
- # waiting for a worker. Returns 0.0 when no operations have completed yet.
497
+ # Average time (in seconds) that completed or skipped operations spent in the
498
+ # queue waiting for a worker. Returns 0.0 when none have been processed yet.
387
499
  # @return [Float]
388
500
  # @api private
389
501
  def average_wait_seconds
@@ -76,7 +76,11 @@ module Phronomy
76
76
  #
77
77
  # Callbacks are NOT fired for deadline-based cancellation (i.e. when
78
78
  # {#cancelled?} returns +true+ due to +@monotonic_deadline+ expiry). Use
79
- # {Runtime#timer_queue} to schedule deadline callbacks.
79
+ # {Phronomy::Concurrency::CancellationScope#deadline_in}, which registers
80
+ # a timer via {Runtime#timer_queue} and calls {#cancel!} on expiry — this
81
+ # fires all +on_cancel+ callbacks automatically. {.timeout_after} is a
82
+ # lightweight alternative that uses lazy clock comparison only and does
83
+ # NOT trigger callbacks on expiry.
80
84
  #
81
85
  # @yield called with no arguments when (or if) the token is cancelled
82
86
  # @return [self]
@@ -9,7 +9,10 @@ module Phronomy
9
9
  # All pools are shut down together by {#shutdown}.
10
10
  # @api private
11
11
  class PoolRegistry
12
- def initialize
12
+ # @param timer_queue_provider [#call, nil] provider passed to every pool
13
+ # @api private
14
+ def initialize(timer_queue_provider: nil)
15
+ @timer_queue_provider = timer_queue_provider
13
16
  @mutex = Mutex.new
14
17
  @pools = {}
15
18
  @default = nil
@@ -24,7 +27,8 @@ module Phronomy
24
27
  @default ||= BlockingAdapterPool.new(
25
28
  name: :default,
26
29
  pool_size: pool_size,
27
- queue_size: queue_size
30
+ queue_size: queue_size,
31
+ timer_queue_provider: @timer_queue_provider
28
32
  )
29
33
  end
30
34
 
@@ -39,7 +43,8 @@ module Phronomy
39
43
  @pools[name.to_sym] ||= BlockingAdapterPool.new(
40
44
  name: name,
41
45
  pool_size: size,
42
- queue_size: queue_size
46
+ queue_size: queue_size,
47
+ timer_queue_provider: @timer_queue_provider
43
48
  )
44
49
  end
45
50
  end