farce 0.2.0 → 0.3.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.
data/lib/farce/pool.rb CHANGED
@@ -36,297 +36,9 @@ module Farce
36
36
  #
37
37
  # # Schedule some work
38
38
  # pool.schedule { MyClass.do_something }
39
- class Pool < Farce::Abstract::Scheduler
40
- include Internal::MarshalSupport::Reject
39
+ class Pool < Farce::Abstract::Pool
41
40
  include Shareable::Unfreezable
42
- include Internal::Inspect
43
41
 
44
- # @return [Float] Maximum queue wait before adding a worker.
45
- attr_reader :grow_after
46
-
47
- # @return [Integer, nil] Soft task limit for each worker.
48
- attr_reader :max_inflight
49
-
50
- # @return [Integer] Maximum worker count.
51
- attr_reader :max_size
52
-
53
- # @return [Integer] Minimum worker count.
54
- attr_reader :min_size
55
-
56
- # @return [Float, nil] Idle time before an extra worker retires.
57
- attr_reader :shrink_after
58
-
59
- # Create and start an elastic pool using the configured fiber scheduler.
60
- # An explicit constructor block overrides the configured default.
61
- #
62
- # `max_inflight` is a soft limit. A worker may exceed it when its scheduler
63
- # has no runnable fibers or when the pool has reached `max_size`.
64
- #
65
- # @param min_size [Integer] Workers started immediately and retained while open.
66
- # @param max_size [Integer] Maximum workers allowed.
67
- # @param max_inflight [Integer, nil] Soft task limit for each worker.
68
- # @param grow_after [Numeric] Queue wait before adding another worker.
69
- # @param shrink_after [Numeric, nil] Idle time before an extra worker retires.
70
- # @param capacity [Integer, nil] Pending-task capacity.
71
- # @param backend [Symbol] IO backend for the built-in scheduler.
72
- # @yieldreturn [Object] Fiber scheduler constructed in each worker.
73
- def initialize(min_size: 0, max_size: 4, max_inflight: 64,
74
- grow_after: 0.005, shrink_after: 30, capacity: 1024,
75
- backend: Internal::FROZEN_CONFIG.io_backend, &constructor)
76
- constructor ||= Internal::FROZEN_CONFIG.fiber_scheduler_constructor
77
- @min_size = Integer(min_size)
78
- @max_size = Integer(max_size)
79
- @max_inflight = max_inflight && Integer(max_inflight)
80
- @grow_after = duration(grow_after, :grow_after)
81
- @shrink_after = duration(shrink_after, :shrink_after, nil_allowed: true)
82
- validate_sizes
83
-
84
- @backend = backend
85
- @constructor = constructor
86
- @constructor = Ractor.shareable_proc(&constructor) if constructor && !Ractor.shareable?(constructor)
87
- @queue = Internal::Queue.new(capacity: capacity && Integer(capacity), track_age: true)
88
- @state = Internal::Atom.new(:running)
89
- @error = Internal::Atom.new
90
- @worker_count = Internal::Atom.new(0)
91
- @pressure = Internal::Atom.new
92
- @admission = Internal::Signal.new
93
- super()
94
- begin
95
- @min_size.times { start_worker }
96
- rescue Exception # rubocop:disable Lint/RescueException
97
- close
98
- raise
99
- end
100
- end
101
-
102
- # Enqueue a task for any worker in the pool.
103
- #
104
- # Pools have no single owner, so `auto_local` does not select local transfer.
105
- # Explicit local transfer is rejected because another Ractor may run the task.
106
- #
107
- # @!macro modes
108
- # @param args [Array<Object>] Positional arguments passed to the task block.
109
- # @param mode [Symbol] Argument transfer mode.
110
- # @param auto_local [Boolean] Accepted for compatibility with {Scheduler#schedule}.
111
- # @yield [*args] Task to execute.
112
- # @return [Pool] self.
113
- # @raise [PoolClosedError] If the pool is closing or closed.
114
- def schedule(*args, mode: :smart, auto_local: true, &block) # rubocop:disable Lint/UnusedMethodArgument
115
- raise PoolClosedError, "cannot schedule task on a closed pool" if closed?
116
- raise ArgumentError, "local transfer is not supported by a pool" if mode == :local
117
-
118
- task = Internal::ScheduledTask.new(args, block, mode)
119
- @queue.push(task)
120
- arm_scaler
121
- self
122
- rescue ClosedQueueError
123
- raise unless closed?
124
- raise PoolClosedError, "cannot schedule task on a closed pool"
125
- end
126
-
127
- # Stop accepting tasks and drain queued work.
128
- #
129
- # @return [Pool] self.
130
- def close
131
- return self if closed?
132
- @state.compare_and_set(:running, :closing)
133
- @queue.seal
134
- admission_changed
135
- if size.zero?
136
- @queue.closed? ? finish_close : start_worker(drain: true)
137
- end
138
- self
139
- end
140
-
141
- # Return the first worker error, if any.
142
- def error
143
- value = @error.value
144
- value.is_a?(Envelope) ? value.value : value
145
- end
146
-
147
- # Return the current number of running and starting workers.
148
- def size = @worker_count.value
149
-
150
- # Return the current pool state.
151
- def state = @state.value
152
-
153
- # Return whether the pool rejects new tasks.
154
- def closed? = state != :running
155
-
156
- # Return whether the pool is draining queued work.
157
- def closing? = state == :closing
158
-
159
- # Return whether closing workers have drained all admitted submissions.
160
- # @api private
161
- def drained? = closing? && @queue.closed?
162
-
163
- # Return whether all configured workers have been started.
164
- def at_max_size? = size >= @max_size
165
-
166
- # Return the generation used by workers waiting for admission.
167
- # @api private
168
- def admission_generation = @admission.generation
169
-
170
- # Wait for worker capacity or a pool state change.
171
- # @api private
172
- def wait_for_admission(generation) = @admission.wait(generation)
173
-
174
- # Wake workers waiting at their soft admission limit.
175
- # @api private
176
- def admission_changed = @admission.broadcast
177
-
178
- # Record that a task left the shared queue.
179
- # @api private
180
- def task_taken
181
- return if @pressure.value.nil? || at_max_size?
182
- return unless queue_empty?
183
- @pressure.value = nil
184
- arm_scaler if @queue.size.positive?
185
- end
186
-
187
- # Reserve a worker retirement above the minimum size.
188
- # @api private
189
- def retire_worker
190
- retired = false
191
- @worker_count.update do |count|
192
- if state == :running && count > @min_size && queue_empty?
193
- retired = true
194
- count - 1
195
- else
196
- count
197
- end
198
- end
199
- retired
200
- end
201
-
202
- # Remove a stopped worker and preserve its failure.
203
- # @api private
204
- def worker_stopped(error, replace: true)
205
- @worker_count.update { |count| count - 1 }
206
- @error.compare_and_set(nil, Envelope.new(error)) if error
207
- if closing?
208
- if size.zero?
209
- error && !drained? ? start_worker(drain: true) : finish_close
210
- end
211
- elsif state == :running && replace
212
- begin
213
- start_worker while size < @min_size
214
- rescue Exception # rubocop:disable Lint/RescueException
215
- nil
216
- end
217
- arm_scaler if @queue.size.positive?
218
- end
219
- admission_changed
220
- end
221
-
222
- # Recheck delayed queue pressure and grow by one worker.
223
- # @api private
224
- def scale_if_needed(started)
225
- return unless state == :running && @pressure.value == started
226
- age = @queue.oldest_age
227
- return clear_pressure(started) unless age
228
- return reschedule_scaler(started, @grow_after - age) if age < @grow_after
229
-
230
- start_worker unless at_max_size?
231
- return clear_pressure(started) if queue_empty? || at_max_size?
232
-
233
- next_started = @queue.generation
234
- schedule_scaler(next_started) if @pressure.compare_and_set(started, next_started)
235
- rescue Exception => e # rubocop:disable Lint/RescueException
236
- @error.compare_and_set(nil, Envelope.new(e))
237
- end
238
-
239
- # @api private
240
- def inspect_with(inspector)
241
- super do
242
- inspector.attribute(:state, state)
243
- inspector.attribute(:backend, @backend)
244
- inspector.attribute(:size, size)
245
- inspector.attribute(:min_size, @min_size)
246
- inspector.attribute(:max_size, @max_size)
247
- inspector.attribute(:max_inflight, @max_inflight) if @max_inflight
248
- yield if block_given?
249
- end
250
- end
251
-
252
- private
253
-
254
- def validate_sizes
255
- raise ArgumentError, "min_size must be non-negative" if @min_size.negative?
256
- raise ArgumentError, "max_size must be positive" unless @max_size.positive?
257
- raise ArgumentError, "min_size must not exceed max_size" if @min_size > @max_size
258
- raise ArgumentError, "max_inflight must be positive" if @max_inflight && !@max_inflight.positive?
259
- end
260
-
261
- def duration(value, name, nil_allowed: false)
262
- return if value.nil? && nil_allowed
263
- value = Float(value)
264
- raise ArgumentError, "#{name} must be finite and non-negative" if !value.finite? || value.negative?
265
- value
266
- end
267
-
268
- def start_worker(drain: false, only_if_empty: false)
269
- reserved = false
270
- @worker_count.update do |count|
271
- can_start = state == :running || (drain && closing? && !queue_empty?)
272
- if can_start && count < @max_size && (!only_if_empty || count.zero?)
273
- reserved = true
274
- count + 1
275
- else
276
- count
277
- end
278
- end
279
- return false unless reserved
280
-
281
- worker = Internal::PoolWorker.new(self)
282
- scheduler = Scheduler.new(
283
- backend: @backend,
284
- queue: @queue,
285
- pool_worker: worker,
286
- queue_owner: false,
287
- &@constructor
288
- )
289
- scheduler.launch_ractor
290
- admission_changed if at_max_size?
291
- true
292
- rescue Exception => e # rubocop:disable Lint/RescueException
293
- worker ? worker.stopped(e, replace: false) : @worker_count.update { |count| count - 1 }
294
- raise
295
- end
296
-
297
- def arm_scaler
298
- return if at_max_size? || queue_empty? || state != :running
299
- start_worker(only_if_empty: true) if size.zero?
300
- return if at_max_size?
301
-
302
- started = @queue.generation
303
- age = @queue.oldest_age
304
-
305
- return unless age
306
- return unless @pressure.compare_and_set(nil, started)
307
-
308
- remaining = @grow_after - age
309
- remaining = 0 if remaining.negative?
310
-
311
- schedule_scaler(started, remaining)
312
- end
313
-
314
- def schedule_scaler(started, delay = @grow_after)
315
- Internal::PoolSupervisor.schedule(self, started, delay)
316
- end
317
-
318
- def reschedule_scaler(started, delay)
319
- next_started = @queue.generation
320
- schedule_scaler(next_started, delay) if @pressure.compare_and_set(started, next_started)
321
- end
322
-
323
- def clear_pressure(started)
324
- return unless @pressure.compare_and_set(started, nil)
325
- arm_scaler if @queue.size.positive?
326
- end
327
-
328
- def queue_empty? = @queue.size.zero? # rubocop:disable Style/ZeroLengthPredicate
329
-
330
- def finish_close = @state.compare_and_set(:closing, :closed)
42
+ private def publish_pool = publish_shareable
331
43
  end
332
44
  end
data/lib/farce/proxy.rb CHANGED
@@ -216,7 +216,7 @@ module Farce
216
216
  new(object, register:, scheduler:)
217
217
  end
218
218
 
219
- def self.owner_scheduler = ::Farce::ThreadScheduler.new { |*args, &block| ::Farce::Ractor.new(*args, &block) }
219
+ def self.owner_scheduler = ::Farce::ThreadExecutor.new { |*args, &block| ::Farce::Ractor.new(*args, &block) }
220
220
  private_class_method :owner_scheduler
221
221
 
222
222
  def self.schedule_initializer(scheduler, *)
@@ -254,7 +254,7 @@ module Farce
254
254
 
255
255
  # @param object [BasicObject] the object to be managed by the proxy
256
256
  # @param register [Farce::Proxy::Register, nil] the register to use
257
- # @param scheduler [Farce::Abstract::Scheduler, nil] the scheduler to use
257
+ # @param scheduler [Farce::Abstract::Executor, nil] the scheduler to use
258
258
  def initialize(object, register: nil, scheduler: nil)
259
259
  @token = ::Object.new.freeze # cross-ractor finalizers don't work reliably across Ruby versions
260
260
  @supervisor = Supervisor.new(register || REGISTER, scheduler || ::Farce, ::Farce::WeakValue.new(@token))