Class: Farce::Abstract::Scheduler Abstract
- Includes:
- Internal::Inspect
- Defined in:
- lib/farce/abstract/scheduler.rb
Overview
Use Scheduler, Unshared::Scheduler, or Local::Scheduler.
A task scheduler for one worker or an installed fiber scheduler.
Use Scheduler.create to start a worker, or install an instance on the current
thread with Fiber.set_scheduler. #close rejects new submissions and
drains queued tasks. A scheduler can only be launched or installed once.
Direct Known Subclasses
Class Method Summary collapse
-
.create(executor = nil, name: nil, priority: nil) ⇒ BasicObject
Creates a new scheduler and starts it in a background ractor, thread, or custom executor.
-
.current ⇒ Scheduler?
Returns a Farce scheduler for the current thread's installed fiber scheduler.
Instance Method Summary collapse
-
#alive? ⇒ Boolean
Whether the dispatcher is in its
:runningstate. -
#capacity ⇒ BasicObject
Pending-task capacity.
-
#capacity=(value) ⇒ BasicObject
Resize the pending queue without discarding tasks.
-
#close ⇒ Scheduler
Requests shutdown and rejects further submissions.
-
#closed? ⇒ Boolean
Whether new submissions are rejected, including while closing or after a dispatch error.
-
#error ⇒ Exception?
The error encountered by the scheduler, if any.
-
#external_scheduler ⇒ Object?
Retrieves the constructed external scheduler from its owning Ractor.
-
#initialize(capacity: 1024, backend: Internal::FROZEN_CONFIG.io_backend, queue: nil, external: false, pool_worker: nil, queue_owner: true, &constructor) ⇒ Scheduler
constructor
Creates a scheduler without starting a worker.
-
#launch(executor = nil, name: nil, priority: nil) ⇒ Ractor, ...
Starts dispatching tasks in a new worker and waits for setup to leave its launching/setup states before returning.
-
#launch_ractor(name: nil, priority: nil) ⇒ Ractor
Launches a Ractor worker.
-
#launch_thread(name: nil, priority: nil) ⇒ Thread
Launches a Thread worker.
-
#local?(wait: true) ⇒ Boolean
Whether the executor is local for the current Ractor.
-
#owner ⇒ Ractor?
The owning Ractor, or nil before ownership is assigned.
-
#owner=(value) ⇒ BasicObject
Assigns ownership during setup.
-
#schedule(*args, mode: :smart, auto_local: true, timeout: nil) {|*args| ... } ⇒ self?
Enqueues a task, waiting for queue space if necessary.
-
#state ⇒ Symbol
The current state of the scheduler.
-
#try_schedule(*args, mode: :smart, auto_local: true) ⇒ self?
Enqueues a task without waiting for queue space.
-
#wraps_external? ⇒ Boolean
Whether a fiber scheduler constructor was supplied.
Methods inherited from Executor
#close_all, #execute, #ractor_shareable?
Methods included from Internal::Noncopyable
Constructor Details
#initialize(capacity: 1024, backend: Farce.config.io_backend) ⇒ Scheduler #initialize(capacity: 1024, &block) ⇒ Scheduler
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/farce/abstract/scheduler.rb', line 156 def initialize(capacity: 1024, backend: Internal::FROZEN_CONFIG.io_backend, queue: nil, external: false, pool_worker: nil, queue_owner: true, &constructor) constructor ||= Internal::FROZEN_CONFIG.fiber_scheduler_constructor @capacity = capacity ? Integer(capacity) : nil @backend = backend @constructor = prepare_constructor(constructor) @external = external || !constructor.nil? @owner = Internal::Atom.new(nil) @state = Internal::Atom.new(:initialized) @error = Internal::Atom.new @queue = queue || new_queue(capacity: @capacity) @pool_worker = pool_worker @queue_owner = queue_owner super() end |
Class Method Details
.create(executor = nil, name: nil, priority: nil, capacity: 1024, backend: Farce.config.io_backend) ⇒ Scheduler .create(executor = nil, name: nil, priority: nil, capacity: 1024, &block) ⇒ Scheduler
Creates a new scheduler and starts it in a background ractor, thread, or custom executor.
120 121 122 |
# File 'lib/farce/abstract/scheduler.rb', line 120 def self.create(executor = nil, name: nil, priority: nil, **, &) new(**, &).tap { it.launch(executor, name:, priority:) } end |
.current ⇒ Scheduler?
Returns a Farce scheduler for the current thread's installed fiber scheduler.
Returns nil when no fiber scheduler is installed.
Creates and installs a wrapper when necessary, starting its dispatcher on that scheduler.
Additional keyword arguments are forwarded to #initialize when wrapping.
55 56 57 58 59 60 61 62 63 64 |
# File 'lib/farce/abstract/scheduler.rb', line 55 def self.current(**) return unless Fiber.respond_to?(:scheduler) return unless scheduler = Fiber.scheduler register = Internal::Storage.store_if_absent(self) { Internal::Storage.new } register.store_if_absent(scheduler) do wrapper = new(**, external: true) wrapper.__send__(:set_scheduler!, scheduler:) wrapper end end |
Instance Method Details
#alive? ⇒ Boolean
Returns Whether the dispatcher is in its :running state.
392 |
# File 'lib/farce/abstract/scheduler.rb', line 392 def alive? = @state.value == :running |
#capacity ⇒ BasicObject
Pending-task capacity. Nil means the queue is unbounded.
173 |
# File 'lib/farce/abstract/scheduler.rb', line 173 def capacity = @queue.capacity |
#capacity=(value) ⇒ BasicObject
Resize the pending queue without discarding tasks. JRuby and TruffleRuby cannot switch between bounded and unbounded queues.
177 178 179 |
# File 'lib/farce/abstract/scheduler.rb', line 177 def capacity=(value) @queue.capacity = value.nil? ? nil : Integer(value) end |
#close ⇒ Scheduler
Requests shutdown and rejects further submissions. Repeated calls are harmless. The dispatcher drains queued tasks before closing the queue. This method does not wait for dispatch or running tasks to finish.
This wakes a dispatcher blocked on an empty queue. It does not directly close the underlying fiber scheduler.
361 362 363 364 365 366 367 368 369 370 |
# File 'lib/farce/abstract/scheduler.rb', line 361 def close until closed? state = @state.value @state.compare_and_set(state, :closing) unless CLOSED_STATES.include?(state) end # if the scheduler never acquired an owner, #schedule might be blocked @owner.compare_and_set(nil, Ractor.current) @queue_owner ? @queue.seal : wake_dispatcher self end |
#closed? ⇒ Boolean
Returns Whether new submissions are rejected, including while closing or after a dispatch error. Does not imply all tasks have finished.
388 |
# File 'lib/farce/abstract/scheduler.rb', line 388 def closed? = CLOSED_STATES.include?(@state.value) |
#error ⇒ Exception?
Returns The error encountered by the scheduler, if any.
182 183 184 185 |
# File 'lib/farce/abstract/scheduler.rb', line 182 def error value = @error.value value.is_a?(Envelope) ? value.value : value end |
#external_scheduler ⇒ Object?
Retrieves the constructed external scheduler from its owning Ractor.
299 300 301 302 303 |
# File 'lib/farce/abstract/scheduler.rb', line 299 def external_scheduler return unless wraps_external? return Internal::Storage[self] if owner.nil? || owner == Ractor.current raise Ractor::IsolationError, "external_scheduler is owned by a different Ractor" end |
#launch(executor = nil, name: nil, priority: nil) ⇒ Ractor, ...
Starts dispatching tasks in a new worker and waits for setup to leave its launching/setup states before returning. Returns the created worker instance.
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/farce/abstract/scheduler.rb', line 231 def launch(executor = nil, name: nil, priority: nil) raise "Scheduler may not be reused" unless @state.compare_and_set(:initialized, :launching) executor ||= Internal.native_ractors? ? Ractor : Thread priority &&= Integer(priority) name &&= -String(name) callback = launch_callback return executor.new(self, priority, name, &callback) unless executor <= Ractor # Requiring an autoloaded implementation from a new Ractor can deadlock # while its launcher waits for setup to finish. Internal.const_get(:FiberScheduler, false) Internal.const_get(:Storage, false) Internal.const_get(:ThreadPool, false) executor.new(self, priority, name, name:, &callback) rescue Exception => e # rubocop:disable Lint/RescueException @state.value = :error @error.value = Envelope.new(e) raise ensure # if we return the thread too early, someone could kill it while another threads blocks on a schedule call if System.windows? && executor <= Ractor sleep 0.001 while @state.value == :launching sleep 0.001 while @state.value == :setup else @state.wait_until_changed(:launching) @state.wait_until_changed(:setup) end end |
#launch_ractor(name: nil, priority: nil) ⇒ Ractor
192 |
# File 'lib/farce/abstract/scheduler.rb', line 192 def launch_ractor(...) = launch(Ractor, ...) |
#launch_thread(name: nil, priority: nil) ⇒ Thread
199 |
# File 'lib/farce/abstract/scheduler.rb', line 199 def launch_thread(...) = launch(Thread, ...) |
#local?(wait: true) ⇒ Boolean
Returns Whether the executor is local for the current Ractor.
349 350 351 352 |
# File 'lib/farce/abstract/scheduler.rb', line 349 def local?(wait: true) owner = wait ? @owner.wait_until_non_nil : @owner.value owner == Ractor.current end |
#owner ⇒ Ractor?
Returns The owning Ractor, or nil before ownership is assigned.
262 |
# File 'lib/farce/abstract/scheduler.rb', line 262 def owner = @owner.value |
#owner=(value) ⇒ BasicObject
Assigns ownership during setup. An existing owner cannot be changed. This is used by schedule to determine if a task is scheduled locally or remotely.
If #schedule is called without setting auto_local to false, it will block until the owner can be determined.
Setting the owner explicitly avoids blocking on ownership determination.
284 285 286 287 288 289 |
# File 'lib/farce/abstract/scheduler.rb', line 284 def owner=(value) raise ArgumentError, "owner must be a Ractor" unless value.nil? || value.is_a?(Ractor) return if @owner.compare_and_set(nil, value) return if owner == value raise ArgumentError, "owner has already been set" end |
#schedule(*args, mode: :smart, auto_local: true, timeout: nil) {|*args| ... } ⇒ self?
Enqueues a task, waiting for queue space if necessary. Returns after submission. It does not wait for task completion or return the task's value.
With auto_local enabled, waits for ownership to be assigned and uses :local
mode whenever the caller and owner are in the same Ractor, including different
threads in that Ractor.
Local tasks retain their block and arguments.
Otherwise, the block must be convertible to a Ractor-shareable proc. Pass task data as arguments so the selected transfer mode can be applied to it.
A timeout limits the combined wait for ownership and queue space.
Expiry returns nil without enqueuing the task. Accepted tasks have no execution timeout.
333 |
# File 'lib/farce/abstract/scheduler.rb', line 333 def schedule(...) = enqueue_task(false, ...) |
#state ⇒ Symbol
Returns The current state of the scheduler. One of:
:initialized: The scheduler has been created but not yet launched.:launching: The scheduler is in the process of being launched on a background worker.:setup: The scheduler is in the process of being setting up a fiber scheduler.:running: The scheduler is actively dispatching tasks.:closing: The scheduler has been requested to shut down and is draining its queue.:closed: The scheduler has finished shutting down.:error: The scheduler encountered an error during dispatch.
381 |
# File 'lib/farce/abstract/scheduler.rb', line 381 def state = @state.value |
#try_schedule(*args, mode: :smart, auto_local: true) ⇒ self?
Enqueues a task without waiting for queue space.
Arguments and transfer modes follow #schedule. With auto_local enabled,
it still waits for ownership to be assigned.
344 345 346 |
# File 'lib/farce/abstract/scheduler.rb', line 344 def try_schedule(*, mode: :smart, auto_local: true, &) enqueue_task(true, *, mode:, auto_local:, &) end |
#wraps_external? ⇒ Boolean
Returns Whether a fiber scheduler constructor was supplied.
292 |
# File 'lib/farce/abstract/scheduler.rb', line 292 def wraps_external? = @external |