farce 0.1.0-x86-linux-gnu → 0.3.0-x86-linux-gnu

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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -1
  3. data/README.md +111 -17
  4. data/docs/gems/dry-types.md +8 -8
  5. data/docs/modes.md +75 -15
  6. data/docs/scopes.md +3 -1
  7. data/docs/variants.md +18 -0
  8. data/ext/farce/atom.c +37 -22
  9. data/ext/farce/bounded_map.c +17 -13
  10. data/ext/farce/containers.h +15 -0
  11. data/ext/farce/counter.c +13 -18
  12. data/ext/farce/exchanger.c +5 -1
  13. data/ext/farce/farce.c +1 -1
  14. data/ext/farce/flag.c +39 -3
  15. data/ext/farce/io.c +3 -3
  16. data/ext/farce/lock.c +17 -23
  17. data/ext/farce/map.c +88 -66
  18. data/ext/farce/priority_queue.c +55 -43
  19. data/ext/farce/queue.c +10 -4
  20. data/ext/farce/reactor.c +52 -25
  21. data/ext/farce/reactor.h +1 -1
  22. data/ext/farce/signal.c +5 -1
  23. data/ext/farce/transaction.c +20 -24
  24. data/ext/farce/tree_map.c +33 -18
  25. data/ext/farce/trie.c +81 -43
  26. data/ext/farce/unshareable.c +168 -19
  27. data/ext/farce/unshared_io_pool.h +21 -12
  28. data/ext/farce/unshared_signal.c +13 -2
  29. data/ext/farce/unshared_wait.h +3 -0
  30. data/ext/farce/vector.c +82 -18
  31. data/ext/farce/weak_map.c +75 -63
  32. data/lib/farce/_yard/macros.rb +18 -3
  33. data/lib/farce/abstract/executor.rb +130 -0
  34. data/lib/farce/abstract/flag.rb +73 -0
  35. data/lib/farce/abstract/pool.rb +334 -0
  36. data/lib/farce/abstract/queue.rb +4 -3
  37. data/lib/farce/abstract/scheduler.rb +539 -93
  38. data/lib/farce/abstract/set.rb +17 -5
  39. data/lib/farce/abstract/sorted_set.rb +2 -2
  40. data/lib/farce/abstract/timer_queue.rb +49 -10
  41. data/lib/farce/abstract/vector.rb +510 -28
  42. data/lib/farce/atom.rb +1 -1
  43. data/lib/farce/class_mirror.rb +20 -7
  44. data/lib/farce/config.rb +7 -1
  45. data/lib/farce/engine/jruby/signal.rb +7 -4
  46. data/lib/farce/engine/jvm/flag.rb +15 -3
  47. data/lib/farce/engine/ruby/3.4/farce.so +0 -0
  48. data/lib/farce/engine/ruby/4.0/farce.so +0 -0
  49. data/lib/farce/engine/ruby/shared/require_hook.rb +18 -0
  50. data/lib/farce/engine/ruby.rb +7 -0
  51. data/lib/farce/engine/shared/main_scheduler.rb +2 -2
  52. data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
  53. data/lib/farce/engine/shared/proxy_owner.rb +1 -1
  54. data/lib/farce/engine/shared/unshared_atom.rb +9 -8
  55. data/lib/farce/engine/shared/unshared_vector.rb +31 -4
  56. data/lib/farce/engine/shared/vector.rb +8 -0
  57. data/lib/farce/engine/shared/weak_map/index.rb +14 -1
  58. data/lib/farce/engine/shared.rb +9 -0
  59. data/lib/farce/engine/truffleruby/native/flag.rb +15 -3
  60. data/lib/farce/envelope.rb +54 -7
  61. data/lib/farce/exchanger.rb +1 -1
  62. data/lib/farce/flag.rb +2 -0
  63. data/lib/farce/integrations/active_support.rb +1 -1
  64. data/lib/farce/integrations/bson.rb +1 -1
  65. data/lib/farce/integrations/cbor.rb +1 -1
  66. data/lib/farce/integrations/concurrent.rb +3 -3
  67. data/lib/farce/integrations/date.rb +8 -0
  68. data/lib/farce/integrations/dry_types.rb +7 -4
  69. data/lib/farce/integrations/json.rb +2 -2
  70. data/lib/farce/integrations/msgpack.rb +1 -1
  71. data/lib/farce/integrations/oj.rb +3 -4
  72. data/lib/farce/integrations/psych.rb +1 -1
  73. data/lib/farce/integrations/ractor_sharing.rb +1 -1
  74. data/lib/farce/integrations/sorted_set.rb +1 -1
  75. data/lib/farce/integrations/weakref.rb +9 -1
  76. data/lib/farce/integrations/yajl.rb +3 -3
  77. data/lib/farce/integrations.rb +33 -23
  78. data/lib/farce/internal/flag_waiting.rb +30 -0
  79. data/lib/farce/internal/freeze.rb +8 -3
  80. data/lib/farce/internal/lease_initialization.rb +1 -1
  81. data/lib/farce/internal/local_executor_methods.rb +33 -0
  82. data/lib/farce/internal/local_scheduled_task.rb +27 -0
  83. data/lib/farce/internal/map_value_modes.rb +1 -1
  84. data/lib/farce/internal/scheduled_task.rb +1 -1
  85. data/lib/farce/internal/scheduler_lifecycle.rb +1 -1
  86. data/lib/farce/internal/thread_pool_supervisor.rb +45 -0
  87. data/lib/farce/internal/unshared_pool_worker.rb +14 -0
  88. data/lib/farce/internal.rb +14 -0
  89. data/lib/farce/lazy.rb +4 -4
  90. data/lib/farce/lazy_ref.rb +3 -3
  91. data/lib/farce/lfu_map.rb +1 -1
  92. data/lib/farce/local/flag.rb +4 -0
  93. data/lib/farce/local/pool.rb +111 -0
  94. data/lib/farce/local/scheduler.rb +219 -0
  95. data/lib/farce/local/scoped.rb +1 -1
  96. data/lib/farce/lru_map.rb +1 -1
  97. data/lib/farce/map.rb +2 -2
  98. data/lib/farce/mode_manager/smart_rule.rb +138 -0
  99. data/lib/farce/mode_manager.rb +12 -3
  100. data/lib/farce/molecule.rb +2 -2
  101. data/lib/farce/pool.rb +2 -290
  102. data/lib/farce/port.rb +4 -4
  103. data/lib/farce/priority_queue.rb +1 -1
  104. data/lib/farce/proxy/register.rb +6 -0
  105. data/lib/farce/proxy/supervisor.rb +1 -1
  106. data/lib/farce/proxy.rb +9 -4
  107. data/lib/farce/queue.rb +1 -1
  108. data/lib/farce/scheduler.rb +1 -502
  109. data/lib/farce/set.rb +1 -1
  110. data/lib/farce/sorted_set.rb +1 -1
  111. data/lib/farce/task.rb +285 -0
  112. data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
  113. data/lib/farce/timer_queue.rb +20 -6
  114. data/lib/farce/transaction/vector.rb +43 -9
  115. data/lib/farce/transaction/wrapper.rb +14 -0
  116. data/lib/farce/tree_map.rb +1 -1
  117. data/lib/farce/unsafe/lfu_map.rb +2 -0
  118. data/lib/farce/unsafe/lru_map.rb +2 -0
  119. data/lib/farce/unsafe/tree_map.rb +2 -0
  120. data/lib/farce/unshareable.rb +8 -19
  121. data/lib/farce/unshared/atom.rb +3 -0
  122. data/lib/farce/unshared/lfu_map.rb +2 -0
  123. data/lib/farce/unshared/lru_map.rb +2 -0
  124. data/lib/farce/unshared/map.rb +3 -0
  125. data/lib/farce/unshared/pool.rb +44 -0
  126. data/lib/farce/unshared/scheduler.rb +89 -0
  127. data/lib/farce/unshared/set.rb +1 -0
  128. data/lib/farce/unshared/sorted_set.rb +1 -0
  129. data/lib/farce/unshared/tree_map.rb +5 -1
  130. data/lib/farce/unshared/vector.rb +3 -0
  131. data/lib/farce/unshared/weak_atom.rb +5 -1
  132. data/lib/farce/unshared/weak_key_map.rb +3 -0
  133. data/lib/farce/unshared/weak_map.rb +3 -0
  134. data/lib/farce/unshared/weak_set.rb +1 -0
  135. data/lib/farce/unshared/weak_value_map.rb +3 -0
  136. data/lib/farce/vector.rb +44 -19
  137. data/lib/farce/version.rb +1 -1
  138. data/lib/farce.rb +20 -25
  139. metadata +17 -2
@@ -41,6 +41,14 @@ module Farce
41
41
  # @return [Boolean] whether the replacement succeeded
42
42
  # @raise [ArgumentError] if either argument is not true or false
43
43
  #
44
+ # @!method wait_until_changed(expected, timeout: nil)
45
+ # Wait until the current value differs from the expected value.
46
+ # @param expected [Boolean] the value to wait to change from
47
+ # @param timeout [Numeric, nil] the total seconds available
48
+ # @yield called when the timeout expires
49
+ # @return [Boolean, BasicObject, nil] the changed value or timeout fallback
50
+ # @raise [ArgumentError] if expected is not true or false
51
+ #
44
52
  # @!method toggle
45
53
  # Atomically invert the value.
46
54
  # @return [Boolean] the new value
@@ -51,6 +59,71 @@ module Farce
51
59
  include Internal::ValueSerialization
52
60
  include Internal::Inspect
53
61
 
62
+ # Wait until the flag becomes true.
63
+ # @param timeout [Numeric, nil] the total seconds available
64
+ # @yield called when the timeout expires
65
+ # @return [Boolean, BasicObject, nil] the changed value or timeout fallback
66
+ def wait_until_set(timeout: nil, &) = wait_until_changed(false, timeout:, &)
67
+ alias wait_until_true wait_until_set
68
+ alias wait_while_unset wait_until_set
69
+ alias wait_while_false wait_until_set
70
+
71
+ # Wait until the flag becomes false.
72
+ # @param timeout [Numeric, nil] the total seconds available
73
+ # @yield called when the timeout expires
74
+ # @return [Boolean, BasicObject, nil] the changed value or timeout fallback
75
+ def wait_until_unset(timeout: nil, &) = wait_until_changed(true, timeout:, &)
76
+ alias wait_until_false wait_until_unset
77
+ alias wait_while_set wait_until_unset
78
+ alias wait_while_true wait_until_unset
79
+
80
+ # Wait until a block condition matches the current value.
81
+ # One timeout budget covers all checks and waits. The block is not interrupted.
82
+ # @yieldparam value [Boolean] the current value
83
+ # @yieldreturn [Boolean] whether the value matches
84
+ # @param timeout [Numeric, nil] the total seconds available
85
+ # @return [Boolean, nil] the matching value, or nil on timeout
86
+ # @raise [LocalJumpError] if no block is given
87
+ def wait_until(timeout: nil, &) = Internal.wait_until(self, timeout:, &)
88
+
89
+ # Wait while the block returns a truthy value.
90
+ # @yieldparam value [Boolean] the current value
91
+ # @yieldreturn [BasicObject] a truthy value to keep waiting, or nil or false to stop
92
+ # @param timeout [Numeric, nil] the total seconds available
93
+ # @return [Boolean, nil] the value when the condition becomes false, or nil on timeout
94
+ # @raise [LocalJumpError] if no block is given
95
+ def wait_while(timeout: nil)
96
+ raise LocalJumpError, "no block given" unless block_given?
97
+ wait_until(timeout:) { |value| !yield(value) }
98
+ end
99
+
100
+ # Wait while `object === value` is true.
101
+ # @param object [#===] the pattern to stop matching
102
+ # @param timeout [Numeric, nil] the total seconds available
103
+ # @return [Boolean, nil] the first nonmatching value, or nil on timeout
104
+ def wait_while_match(object, timeout: nil)
105
+ wait_while(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
106
+ end
107
+
108
+ # (see #wait_until_changed)
109
+ def wait_while_value(...) = wait_until_changed(...)
110
+
111
+ # Wait until `object == value` is true.
112
+ # @param object [BasicObject] the value to compare with the current value
113
+ # @param timeout [Numeric, nil] the total seconds available
114
+ # @return [Boolean, nil] the matching value, or nil on timeout
115
+ def wait_until_value(object, timeout: nil)
116
+ wait_until(timeout:) { |value| object == value }
117
+ end
118
+
119
+ # Wait until `object === value` is true.
120
+ # @param object [#===] the pattern to match
121
+ # @param timeout [Numeric, nil] the total seconds available
122
+ # @return [Boolean, nil] the matching value, or nil on timeout
123
+ def wait_until_match(object, timeout: nil)
124
+ wait_until(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
125
+ end
126
+
54
127
  # @api private
55
128
  def inspect_with(inspector)
56
129
  begin
@@ -0,0 +1,334 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Abstract
7
+ # A task scheduler with a dynamically sized pool of workers.
8
+ #
9
+ # Pools accept work through {#schedule}. {#close} rejects new tasks and drains
10
+ # queued work. Concrete pools use either Ractor or Thread workers.
11
+ #
12
+ # @abstract Use {Farce::Pool}, {Unshared::Pool}, or {Local::Pool}.
13
+ class Pool < Executor
14
+ include Internal::Inspect
15
+
16
+ # @return [Float] Maximum queue wait before adding a worker.
17
+ attr_reader :grow_after
18
+
19
+ # @return [Integer, nil] Soft task limit for each worker.
20
+ attr_reader :max_inflight
21
+
22
+ # @return [Integer] Maximum worker count.
23
+ attr_reader :max_size
24
+
25
+ # @return [Integer] Minimum worker count.
26
+ attr_reader :min_size
27
+
28
+ # @return [Float, nil] Idle time before an extra worker retires.
29
+ attr_reader :shrink_after
30
+
31
+ # Create and start an elastic pool using the configured fiber scheduler.
32
+ # An explicit constructor block overrides the configured default.
33
+ #
34
+ # `max_inflight` is a soft limit. A worker may exceed it when its scheduler
35
+ # has no runnable fibers or when the pool has reached `max_size`.
36
+ #
37
+ # @param min_size [Integer] Workers started immediately and retained while open.
38
+ # @param max_size [Integer] Maximum workers allowed.
39
+ # @param max_inflight [Integer, nil] Soft task limit for each worker.
40
+ # @param grow_after [Numeric] Queue wait before adding another worker.
41
+ # @param shrink_after [Numeric, nil] Idle time before an extra worker retires.
42
+ # @param capacity [Integer, nil] Pending-task capacity.
43
+ # @param backend [Symbol] IO backend for the built-in scheduler.
44
+ # @yieldreturn [Object] Fiber scheduler constructed in each worker.
45
+ def initialize(min_size: 0, max_size: 4, max_inflight: 64,
46
+ grow_after: 0.005, shrink_after: 30, capacity: 1024,
47
+ backend: Internal::FROZEN_CONFIG.io_backend, &constructor)
48
+ constructor ||= Internal::FROZEN_CONFIG.fiber_scheduler_constructor
49
+ initialize_configuration(min_size:, max_size:, max_inflight:, grow_after:, shrink_after:, backend:)
50
+ @constructor = prepare_constructor(constructor)
51
+ @queue = new_queue(capacity: capacity && Integer(capacity))
52
+ @state = Internal::Atom.new(:running)
53
+ @error = Internal::Atom.new
54
+ @worker_count = Internal::Atom.new(0)
55
+ @pressure = Internal::Atom.new
56
+ @admission = Internal::Signal.new
57
+ super()
58
+ publish_pool
59
+ begin
60
+ @min_size.times { start_worker }
61
+ rescue Exception # rubocop:disable Lint/RescueException
62
+ close
63
+ raise
64
+ end
65
+ end
66
+
67
+ # Enqueue a task for any worker in the pool.
68
+ #
69
+ # Ractor pools reject local transfer. Thread pools preserve the block and
70
+ # arguments. With `auto_local: false`, they accept only local or smart modes.
71
+ #
72
+ # @overload schedule(*args, mode: :smart, auto_local: true)
73
+ # @!macro modes
74
+ # @param args [Array<Object>] Positional arguments passed to the task block.
75
+ # @param mode [Symbol] Argument transfer mode.
76
+ # @param auto_local [Boolean] Whether to use local transfer in a thread pool.
77
+ # @yield [*args] Task to execute.
78
+ # @return [Pool] self.
79
+ # @raise [PoolClosedError] If the pool is closing or closed.
80
+ def schedule(...) = enqueue_task(false, ...)
81
+
82
+ # Enqueues a task without waiting for queue space.
83
+ # Arguments and transfer modes follow {#schedule}.
84
+ #
85
+ # @overload try_schedule(*args, mode: :smart, auto_local: true)
86
+ # @param (see Farce::Abstract::Executor#schedule)
87
+ # @return [self, nil] self when submitted, or nil when the queue is full.
88
+ def try_schedule(...) = enqueue_task(true, ...)
89
+
90
+ # Stop accepting tasks and drain queued work.
91
+ #
92
+ # @return [Pool] self.
93
+ def close
94
+ return self if closed?
95
+ @state.compare_and_set(:running, :closing)
96
+ @queue.seal
97
+ admission_changed
98
+ if size.zero?
99
+ @queue.closed? ? finish_close : start_worker(drain: true)
100
+ end
101
+ self
102
+ end
103
+
104
+ # Return the first worker error, if any.
105
+ def error
106
+ value = @error.value
107
+ value.is_a?(Envelope) ? value.value : value
108
+ end
109
+
110
+ # Return the current number of running and starting workers.
111
+ def size = @worker_count.value
112
+
113
+ # Return the current pool state.
114
+ def state = @state.value
115
+
116
+ # Return whether the pool rejects new tasks.
117
+ def closed? = state != :running
118
+
119
+ # Return whether the pool is draining queued work.
120
+ def closing? = state == :closing
121
+
122
+ # Return whether closing workers have drained all admitted submissions.
123
+ # @api private
124
+ def drained? = closing? && @queue.closed?
125
+
126
+ # Return whether all configured workers have been started.
127
+ def at_max_size? = size >= @max_size
128
+
129
+ # Return the generation used by workers waiting for admission.
130
+ # @api private
131
+ def admission_generation = @admission.generation
132
+
133
+ # Wait for worker capacity or a pool state change.
134
+ # @api private
135
+ def wait_for_admission(generation) = @admission.wait(generation)
136
+
137
+ # Wake workers waiting at their soft admission limit.
138
+ # @api private
139
+ def admission_changed = @admission.broadcast
140
+
141
+ # Record that a task left the shared queue.
142
+ # @api private
143
+ def task_taken
144
+ return if @pressure.value.nil? || at_max_size?
145
+ return unless queue_empty?
146
+ @pressure.value = nil
147
+ arm_scaler if @queue.size.positive?
148
+ end
149
+
150
+ # Reserve a worker retirement above the minimum size.
151
+ # @api private
152
+ def retire_worker
153
+ retired = false
154
+ @worker_count.update do |count|
155
+ if state == :running && count > @min_size && queue_empty?
156
+ retired = true
157
+ count - 1
158
+ else
159
+ count
160
+ end
161
+ end
162
+ retired
163
+ end
164
+
165
+ # Remove a stopped worker and preserve its failure.
166
+ # @api private
167
+ def worker_stopped(error, replace: true)
168
+ @worker_count.update { |count| count - 1 }
169
+ @error.compare_and_set(nil, Envelope.new(error)) if error
170
+ if closing?
171
+ if size.zero?
172
+ error && !drained? ? start_worker(drain: true) : finish_close
173
+ end
174
+ elsif state == :running && replace
175
+ begin
176
+ start_worker while size < @min_size
177
+ rescue Exception # rubocop:disable Lint/RescueException
178
+ nil
179
+ end
180
+ arm_scaler if @queue.size.positive?
181
+ end
182
+ admission_changed
183
+ end
184
+
185
+ # Recheck delayed queue pressure and grow by one worker.
186
+ # @api private
187
+ def scale_if_needed(started)
188
+ return unless state == :running && @pressure.value == started
189
+ age = @queue.oldest_age
190
+ return clear_pressure(started) unless age
191
+ return reschedule_scaler(started, @grow_after - age) if age < @grow_after
192
+
193
+ start_worker unless at_max_size?
194
+ return clear_pressure(started) if queue_empty? || at_max_size?
195
+
196
+ next_started = @queue.generation
197
+ schedule_scaler(next_started) if @pressure.compare_and_set(started, next_started)
198
+ rescue Exception => e # rubocop:disable Lint/RescueException
199
+ @error.compare_and_set(nil, Envelope.new(e))
200
+ end
201
+
202
+ # @api private
203
+ def inspect_with(inspector)
204
+ super do
205
+ inspector.attribute(:state, state)
206
+ inspector.attribute(:backend, @backend)
207
+ inspector.attribute(:size, size)
208
+ inspector.attribute(:min_size, @min_size)
209
+ inspector.attribute(:max_size, @max_size)
210
+ inspector.attribute(:max_inflight, @max_inflight) if @max_inflight
211
+ yield if block_given?
212
+ end
213
+ end
214
+
215
+ private
216
+
217
+ def enqueue_task(non_block, *args, mode: :smart, auto_local: true, &block)
218
+ raise PoolClosedError, "cannot schedule task on a closed pool" if closed?
219
+ raise LocalJumpError, "Cannot schedule without a block" unless block
220
+
221
+ task = prepare_task(args, block, mode, auto_local)
222
+ return unless non_block ? @queue.try_push(task) : @queue.push(task)
223
+ arm_scaler
224
+ self
225
+ rescue ClosedQueueError
226
+ raise unless closed?
227
+ raise PoolClosedError, "cannot schedule task on a closed pool"
228
+ end
229
+
230
+ def initialize_configuration(min_size:, max_size:, max_inflight:, grow_after:, shrink_after:, backend:)
231
+ @min_size = Integer(min_size)
232
+ @max_size = Integer(max_size)
233
+ @max_inflight = max_inflight && Integer(max_inflight)
234
+ @grow_after = duration(grow_after, :grow_after)
235
+ @shrink_after = duration(shrink_after, :shrink_after, nil_allowed: true)
236
+ @backend = backend
237
+ validate_sizes
238
+ end
239
+
240
+ def prepare_constructor(constructor)
241
+ return constructor if !constructor || Ractor.shareable?(constructor)
242
+ Ractor.shareable_proc(&constructor)
243
+ end
244
+
245
+ def prepare_task(args, block, mode, _auto_local)
246
+ raise ArgumentError, "local transfer is not supported by a pool" if mode == :local
247
+ Internal::ScheduledTask.new(args, block, mode)
248
+ end
249
+
250
+ def new_queue(capacity:) = Internal::Queue.new(capacity:, track_age: true)
251
+ def new_worker = Internal::PoolWorker.new(self)
252
+ def new_scheduler(**, &) = Farce::Scheduler.new(**, &)
253
+ def launch_scheduler(scheduler) = scheduler.launch_ractor
254
+
255
+ def validate_sizes
256
+ raise ArgumentError, "min_size must be non-negative" if @min_size.negative?
257
+ raise ArgumentError, "max_size must be positive" unless @max_size.positive?
258
+ raise ArgumentError, "min_size must not exceed max_size" if @min_size > @max_size
259
+ raise ArgumentError, "max_inflight must be positive" if @max_inflight && !@max_inflight.positive?
260
+ end
261
+
262
+ def duration(value, name, nil_allowed: false)
263
+ return if value.nil? && nil_allowed
264
+ value = Float(value)
265
+ raise ArgumentError, "#{name} must be finite and non-negative" if !value.finite? || value.negative?
266
+ value
267
+ end
268
+
269
+ def start_worker(drain: false, only_if_empty: false)
270
+ reserved = false
271
+ @worker_count.update do |count|
272
+ can_start = state == :running || (drain && closing? && !queue_empty?)
273
+ if can_start && count < @max_size && (!only_if_empty || count.zero?)
274
+ reserved = true
275
+ count + 1
276
+ else
277
+ count
278
+ end
279
+ end
280
+ return false unless reserved
281
+
282
+ worker = new_worker
283
+ scheduler = new_scheduler(
284
+ backend: @backend,
285
+ queue: @queue,
286
+ pool_worker: worker,
287
+ queue_owner: false,
288
+ &@constructor
289
+ )
290
+ launch_scheduler(scheduler)
291
+ admission_changed if at_max_size?
292
+ true
293
+ rescue Exception => e # rubocop:disable Lint/RescueException
294
+ worker ? worker.stopped(e, replace: false) : @worker_count.update { |count| count - 1 }
295
+ raise
296
+ end
297
+
298
+ def arm_scaler
299
+ return if at_max_size? || queue_empty? || state != :running
300
+ start_worker(only_if_empty: true) if size.zero?
301
+ return if at_max_size?
302
+
303
+ started = @queue.generation
304
+ age = @queue.oldest_age
305
+
306
+ return unless age
307
+ return unless @pressure.compare_and_set(nil, started)
308
+
309
+ remaining = @grow_after - age
310
+ remaining = 0 if remaining.negative?
311
+
312
+ schedule_scaler(started, remaining)
313
+ end
314
+
315
+ def schedule_scaler(started, delay = @grow_after)
316
+ Internal::PoolSupervisor.schedule(self, started, delay)
317
+ end
318
+
319
+ def reschedule_scaler(started, delay)
320
+ next_started = @queue.generation
321
+ schedule_scaler(next_started, delay) if @pressure.compare_and_set(started, next_started)
322
+ end
323
+
324
+ def clear_pressure(started)
325
+ return unless @pressure.compare_and_set(started, nil)
326
+ arm_scaler if @queue.size.positive?
327
+ end
328
+
329
+ def queue_empty? = @queue.size.zero? # rubocop:disable Style/ZeroLengthPredicate
330
+
331
+ def finish_close = @state.compare_and_set(:closing, :closed)
332
+ end
333
+ end
334
+ end
@@ -98,7 +98,7 @@ module Farce
98
98
  # @raise [Farce::ClosedQueueError] when the queue is closed
99
99
  # @raise [Farce::SealedQueueError] when the queue is sealed but not yet closed
100
100
  # @raise [ThreadError] when the queue is full and non_block is true
101
- # @return [Boolean] `true` if the item was added to the queue
101
+ # @return [BasicObject, false] a truthy object if the item was added, or `false` if the timeout expired
102
102
  #
103
103
  # @!method try_pop
104
104
  # @abstract Subclasses may add additional parameters to this method.
@@ -116,8 +116,9 @@ module Farce
116
116
  # @yield Block called if the queue is at capacity.
117
117
  # @raise [Farce::ClosedQueueError] when the queue is closed
118
118
  # @raise [Farce::SealedQueueError] when the queue is sealed but not yet closed
119
- # @return [Boolean]
120
- # `true` if the item was added to the queue, `false` if the queue was at capacity and no block was given.
119
+ # @return [BasicObject]
120
+ # A truthy object if the item was added, or the fallback result if the queue was at capacity.
121
+ # Returns `false` if the queue was at capacity and no block was given.
121
122
  #
122
123
  # @!method wait_pop(timeout: nil)
123
124
  # @abstract Subclasses may add additional parameters to this method.