farce 0.2.0-arm-linux-gnu → 0.3.0-arm-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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/README.md +39 -1
- data/docs/modes.md +8 -5
- data/docs/scopes.md +3 -1
- data/docs/variants.md +18 -0
- data/lib/farce/abstract/executor.rb +130 -0
- data/lib/farce/abstract/pool.rb +334 -0
- data/lib/farce/abstract/scheduler.rb +539 -93
- data/lib/farce/engine/jruby/signal.rb +7 -4
- data/lib/farce/engine/shared/main_scheduler.rb +2 -2
- data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
- data/lib/farce/engine/shared/proxy_owner.rb +1 -1
- data/lib/farce/engine/shared/unshared_atom.rb +9 -8
- data/lib/farce/engine/shared.rb +9 -0
- data/lib/farce/internal/lease_initialization.rb +1 -1
- data/lib/farce/internal/local_executor_methods.rb +33 -0
- data/lib/farce/internal/local_scheduled_task.rb +27 -0
- data/lib/farce/internal/scheduled_task.rb +1 -1
- data/lib/farce/internal/thread_pool_supervisor.rb +45 -0
- data/lib/farce/internal/unshared_pool_worker.rb +14 -0
- data/lib/farce/internal.rb +14 -0
- data/lib/farce/local/pool.rb +111 -0
- data/lib/farce/local/scheduler.rb +219 -0
- data/lib/farce/pool.rb +2 -290
- data/lib/farce/proxy.rb +2 -2
- data/lib/farce/scheduler.rb +1 -502
- data/lib/farce/task.rb +285 -0
- data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
- data/lib/farce/unshared/pool.rb +44 -0
- data/lib/farce/unshared/scheduler.rb +89 -0
- data/lib/farce/version.rb +1 -1
- data/lib/farce.rb +9 -17
- metadata +13 -2
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
module Farce
|
|
6
6
|
module Internal # :nodoc: all
|
|
7
|
-
class
|
|
7
|
+
class MainThreadExecutor < ThreadExecutor
|
|
8
8
|
# Enter the main emulated Ractor without starting another thread.
|
|
9
9
|
def execute(...) = FakeRactor.with_current(Ractor.main) { super }
|
|
10
10
|
def local?(wait: true) = Ractor.main? # rubocop:disable Lint/UnusedMethodArgument
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
MainScheduler =
|
|
13
|
+
MainScheduler = MainThreadExecutor.new do |*args, &block|
|
|
14
14
|
Thread.new do
|
|
15
15
|
ThreadGroup::Default.add(Thread.current)
|
|
16
16
|
block.call(*args)
|
|
@@ -17,7 +17,7 @@ module Farce
|
|
|
17
17
|
@value = value
|
|
18
18
|
@compare_by_identity = compare_by_identity
|
|
19
19
|
@mutex = Mutex.new
|
|
20
|
-
@signal =
|
|
20
|
+
@signal = Signal.new
|
|
21
21
|
@updating = false
|
|
22
22
|
@updating_fiber = nil
|
|
23
23
|
@updating_thread = nil
|
|
@@ -224,13 +224,14 @@ module Farce
|
|
|
224
224
|
end
|
|
225
225
|
|
|
226
226
|
def wait_for_signal(deadline)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
# Snapshot while holding the state lock so a change during registration is not missed.
|
|
228
|
+
generation = @signal.generation
|
|
229
|
+
begin
|
|
230
|
+
@mutex.unlock
|
|
231
|
+
ReservationWaiting.wait(@signal, generation, deadline)
|
|
232
|
+
ensure
|
|
233
|
+
@mutex.lock unless @mutex.owned?
|
|
234
|
+
end
|
|
234
235
|
end
|
|
235
236
|
|
|
236
237
|
def reject_update_wait!
|
data/lib/farce/engine/shared.rb
CHANGED
|
@@ -13,6 +13,15 @@ module Farce
|
|
|
13
13
|
module Internal # :nodoc: all
|
|
14
14
|
include Autoloads["#{__dir__}/shared"]
|
|
15
15
|
|
|
16
|
+
# Keep thread-only scheduler workers and their tasks in their emulated Ractor.
|
|
17
|
+
def with_scheduler_thread_group
|
|
18
|
+
group = ThreadGroup.new
|
|
19
|
+
FakeRactor::GROUPS[group] = Ractor.current
|
|
20
|
+
yield group
|
|
21
|
+
ensure
|
|
22
|
+
FakeRactor::GROUPS.delete(group)
|
|
23
|
+
end
|
|
24
|
+
|
|
16
25
|
# @api private
|
|
17
26
|
# The shim's shareable? can delegate to a lazy or weak value and change its state.
|
|
18
27
|
# Snapshot metadata uses only structural properties on engines without Ractors.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
module Farce
|
|
6
6
|
# @!visibility private
|
|
7
7
|
module Internal # :nodoc: all
|
|
8
|
-
# Coordinates lazy
|
|
8
|
+
# Coordinates lazy construction without running user code under a storage lock.
|
|
9
9
|
class LeaseInitialization
|
|
10
10
|
def initialize
|
|
11
11
|
@mutex = Mutex.new
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Internal # :nodoc: all
|
|
7
|
+
# The local scheduling and execution interface for Ractor-local executors.
|
|
8
|
+
module LocalExecutorMethods
|
|
9
|
+
LOCAL_MODES = %i[local smart smart_raise smart_move smart_copy].freeze
|
|
10
|
+
private_constant :LOCAL_MODES
|
|
11
|
+
|
|
12
|
+
# @param wait [Boolean] Accepted for compatibility with {Abstract::Executor#local?}.
|
|
13
|
+
# @return [true] Workers run in the calling Ractor.
|
|
14
|
+
def local?(wait: true) = true # rubocop:disable Lint/UnusedMethodArgument
|
|
15
|
+
|
|
16
|
+
# Run a task directly, preserving its block, arguments, and calling fiber.
|
|
17
|
+
# `mode` and `auto_local` follow the same validation as {#schedule}.
|
|
18
|
+
# @param (see Abstract::Executor#execute)
|
|
19
|
+
# @return [self]
|
|
20
|
+
def execute(*, mode: :smart, auto_local: true, &)
|
|
21
|
+
validate_local_mode(mode, auto_local)
|
|
22
|
+
super(*, mode: :local, auto_local: true, &)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def validate_local_mode(mode, auto_local)
|
|
28
|
+
return if auto_local || LOCAL_MODES.include?(mode)
|
|
29
|
+
raise ArgumentError, "only local or smart modes are supported"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Internal # :nodoc: all
|
|
7
|
+
# A Ractor-local task that retains its block and argument references.
|
|
8
|
+
class LocalScheduledTask
|
|
9
|
+
include Noncopyable
|
|
10
|
+
include Farce::Unshareable
|
|
11
|
+
|
|
12
|
+
def initialize(args, block)
|
|
13
|
+
@args = args
|
|
14
|
+
@block = block
|
|
15
|
+
super()
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_proc(completion = nil)
|
|
19
|
+
lambda do
|
|
20
|
+
@block.call(*@args)
|
|
21
|
+
ensure
|
|
22
|
+
completion&.task_finished
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -12,7 +12,7 @@ module Farce
|
|
|
12
12
|
MANAGER = ModeManager.new
|
|
13
13
|
|
|
14
14
|
def initialize(args, block, mode)
|
|
15
|
-
@block = Ractor.shareable_proc(&block)
|
|
15
|
+
@block = Ractor.shareable?(block) ? block : Ractor.shareable_proc(&block)
|
|
16
16
|
@unwrap = false
|
|
17
17
|
@args = args.map! do |arg|
|
|
18
18
|
wrapped = MANAGER.wrap(arg, mode:)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Internal # :nodoc: all
|
|
7
|
+
# A Ractor-local service for delayed pool growth and executor shutdown requests.
|
|
8
|
+
class ThreadPoolSupervisor
|
|
9
|
+
include Farce::Unshareable
|
|
10
|
+
|
|
11
|
+
def self.current = Storage.store_if_absent(self) { new }
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@queue = TimerQueue.new(mode: :raise)
|
|
15
|
+
@closed = Atom.new(false)
|
|
16
|
+
@worker = Thread.new do
|
|
17
|
+
loop do
|
|
18
|
+
entry = @queue.pop
|
|
19
|
+
break if entry == :stop
|
|
20
|
+
pool, token = entry
|
|
21
|
+
pool = pool.value
|
|
22
|
+
token == :close ? pool.close : pool.scale_if_needed(token)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def close_handle(pool) = [@queue, Envelope::Local.new(pool)].freeze
|
|
29
|
+
|
|
30
|
+
def schedule(pool, token, delay)
|
|
31
|
+
raise ::Farce::ClosedQueueError, "pool supervisor is closed" if @closed.value
|
|
32
|
+
@queue.push([Envelope::Local.new(pool), token].freeze, delay:)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def close
|
|
36
|
+
return unless @closed.compare_and_set(false, true)
|
|
37
|
+
@queue.push(:stop, clock: 0)
|
|
38
|
+
@worker.join
|
|
39
|
+
@queue.close
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
at_exit { Storage[ThreadPoolSupervisor]&.close }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Internal # :nodoc: all
|
|
7
|
+
# A worker coordinator pinned to its pool's Ractor.
|
|
8
|
+
class UnsharedPoolWorker < PoolWorker
|
|
9
|
+
include Farce::Unshareable
|
|
10
|
+
|
|
11
|
+
private def publish_shareable = pin_unshareable
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/farce/internal.rb
CHANGED
|
@@ -54,6 +54,19 @@ module Farce
|
|
|
54
54
|
new_self
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# @api private
|
|
58
|
+
# @return [Abstract::Executor, Fiber::Scheduler, nil]
|
|
59
|
+
def local_scheduler(create: false)
|
|
60
|
+
if Fiber.respond_to?(:scheduler) && fiber_scheduler = Fiber.scheduler
|
|
61
|
+
return fiber_scheduler
|
|
62
|
+
end
|
|
63
|
+
return Internal::MainScheduler if Ractor.main?
|
|
64
|
+
return Internal::Storage[:local_scheduler] unless create
|
|
65
|
+
Internal::Storage.store_if_absent(:local_scheduler) do
|
|
66
|
+
Internal.native_ractors? ? Scheduler.create(Thread) : ThreadExecutor.new
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
57
70
|
def timeout_deadline(timeout)
|
|
58
71
|
return if timeout.nil?
|
|
59
72
|
|
|
@@ -101,6 +114,7 @@ module Farce
|
|
|
101
114
|
def prepare_mutable_numeric(_) = nil
|
|
102
115
|
def prepare_map_access(_klass, _kind) = nil
|
|
103
116
|
def finalize_engine = nil
|
|
117
|
+
def with_scheduler_thread_group = yield(ThreadGroup.new)
|
|
104
118
|
|
|
105
119
|
def delegate(from, to, *methods)
|
|
106
120
|
methods.each do |method|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Local
|
|
7
|
+
# A shareable handle with an independent thread pool in each scope.
|
|
8
|
+
#
|
|
9
|
+
# Each pool uses the scaling settings and fiber scheduler configured at
|
|
10
|
+
# construction. Tasks always keep their block and argument references. Only
|
|
11
|
+
# local and smart modes are accepted when `auto_local` is false. A scheduler
|
|
12
|
+
# constructor must be convertible to a shareable proc for use in each Ractor.
|
|
13
|
+
# {#close} drains only the current scope's pool. {#close_all} drains every pool
|
|
14
|
+
# across all scopes and Ractors, and prevents new pools from starting.
|
|
15
|
+
#
|
|
16
|
+
# @example Keep task state within each Ractor
|
|
17
|
+
# pool = Farce::Local::Pool.new(max_size: 2)
|
|
18
|
+
# results = Farce::Unshared::Queue.new
|
|
19
|
+
# pool.schedule { results.push(:done) }
|
|
20
|
+
# results.pop # => :done
|
|
21
|
+
# pool.close_all
|
|
22
|
+
class Pool < Abstract::Pool
|
|
23
|
+
include Shareable::Unfreezable
|
|
24
|
+
include Scoped
|
|
25
|
+
include Internal::LocalExecutorMethods
|
|
26
|
+
|
|
27
|
+
# (see Farce::Abstract::Pool#initialize)
|
|
28
|
+
# @!macro scopes
|
|
29
|
+
# @param scope [Symbol] The scope of the pool. Defaults to `:ractor`.
|
|
30
|
+
def initialize(min_size: 0, max_size: 4, max_inflight: 64,
|
|
31
|
+
grow_after: 0.005, shrink_after: 30, capacity: 1024, scope: :ractor,
|
|
32
|
+
backend: Internal::FROZEN_CONFIG.io_backend, &constructor)
|
|
33
|
+
initialize_configuration(min_size:, max_size:, max_inflight:, grow_after:, shrink_after:, backend:)
|
|
34
|
+
@constructor = prepare_constructor(constructor || Internal::FROZEN_CONFIG.fiber_scheduler_constructor)
|
|
35
|
+
@pools = Strict::Map.new
|
|
36
|
+
@closed_all = Internal::Flag.new(false)
|
|
37
|
+
@initialization_key = Object.new.freeze
|
|
38
|
+
super(scope:, min_size: @min_size, max_size: @max_size, max_inflight: @max_inflight,
|
|
39
|
+
grow_after: @grow_after, shrink_after: @shrink_after, capacity:, backend:)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Stop accepting work in this scope and drain its queued tasks.
|
|
43
|
+
# @return [self]
|
|
44
|
+
def close
|
|
45
|
+
pool = @closed_all.value ? Internal::Storage[self, scope:] : scoped_value
|
|
46
|
+
pool&.close
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Stop accepting work in every scope and Ractor and request draining.
|
|
51
|
+
# Future scopes also reject submissions. Does not wait for running tasks.
|
|
52
|
+
# @return [self]
|
|
53
|
+
def close_all
|
|
54
|
+
return self unless @closed_all.compare_and_set(false, true)
|
|
55
|
+
@pools.each_pair do |pool, queue|
|
|
56
|
+
queue.push([pool, :close].freeze, clock: 0)
|
|
57
|
+
rescue ClosedQueueError
|
|
58
|
+
# The owning Ractor may already have shut down its supervisor.
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
close
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Integer] Running and starting workers in this scope.
|
|
65
|
+
def size = current_pool&.size || 0
|
|
66
|
+
|
|
67
|
+
# @return [Symbol] The current scope's pool state.
|
|
68
|
+
def state = current_pool&.state || :closed
|
|
69
|
+
|
|
70
|
+
# @return [Exception, nil] The first worker error in this scope.
|
|
71
|
+
def error = current_pool&.error
|
|
72
|
+
|
|
73
|
+
# @return [Boolean] Whether this scope rejects new tasks.
|
|
74
|
+
def closed? = @closed_all.value || super
|
|
75
|
+
|
|
76
|
+
Internal.delegate(self, :scoped_value, :drained?, :admission_generation, :wait_for_admission,
|
|
77
|
+
:admission_changed, :task_taken, :retire_worker, :worker_stopped, :scale_if_needed)
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def enqueue_task(non_block, ...)
|
|
82
|
+
raise PoolClosedError, "cannot schedule task on a closed pool" if @closed_all.value
|
|
83
|
+
pool = scoped_value
|
|
84
|
+
submitted = non_block ? pool.try_schedule(...) : pool.schedule(...)
|
|
85
|
+
submitted && self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def current_pool
|
|
89
|
+
@closed_all.value ? Internal::Storage[self, scope:] : scoped_value
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Worker setup can yield. Initialize once when fibers share a scope.
|
|
93
|
+
def scoped_value
|
|
94
|
+
storage = Internal::Storage.scope(scope)
|
|
95
|
+
pool = storage[self]
|
|
96
|
+
return pool if pool
|
|
97
|
+
|
|
98
|
+
initializers = Internal::Storage.store_if_absent(@initialization_key) { Internal::Storage::ThreadSafe.new }
|
|
99
|
+
initializers.store_if_absent(storage) { Internal::LeaseInitialization.new }.fetch { super }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def new_scoped_value(**)
|
|
103
|
+
pool = Unshared::Pool.new(**, &@constructor)
|
|
104
|
+
queue, reference = Internal::ThreadPoolSupervisor.current.close_handle(pool)
|
|
105
|
+
@pools[reference] = queue
|
|
106
|
+
pool.close if @closed_all.value
|
|
107
|
+
pool
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Local
|
|
7
|
+
# A shareable handle with an independent task scheduler in each scope.
|
|
8
|
+
#
|
|
9
|
+
# {.new} leaves each scheduler unstarted. Launch it with {#launch_thread}, or
|
|
10
|
+
# install it with `Fiber.set_scheduler`. {.create} starts a worker thread in
|
|
11
|
+
# each scope when that scope is first used. The default scope is `:ractor`.
|
|
12
|
+
#
|
|
13
|
+
# Tasks retain their blocks and argument references. {#execute} runs directly
|
|
14
|
+
# in the calling fiber. A fiber scheduler
|
|
15
|
+
# constructor must be convertible to a shareable proc for use in each Ractor.
|
|
16
|
+
# {#close} closes the current scope. {#close_all} closes all scopes and prevents
|
|
17
|
+
# new schedulers from starting. {.current} returns an {Unshared::Scheduler}.
|
|
18
|
+
#
|
|
19
|
+
# @example Start one worker in each Ractor
|
|
20
|
+
# scheduler = Farce::Local::Scheduler.create
|
|
21
|
+
# results = Farce::Unshared::Queue.new
|
|
22
|
+
# scheduler.schedule { results.push(:done) }
|
|
23
|
+
# results.pop # => :done
|
|
24
|
+
# scheduler.close_all
|
|
25
|
+
class Scheduler < Abstract::Scheduler
|
|
26
|
+
include Shareable::Unfreezable
|
|
27
|
+
include Scoped
|
|
28
|
+
include Internal::LocalExecutorMethods
|
|
29
|
+
|
|
30
|
+
# Returns the current thread's unshared scheduler, or nil when none is installed.
|
|
31
|
+
# @param (see Abstract::Scheduler.current)
|
|
32
|
+
# @return [Unshared::Scheduler, nil]
|
|
33
|
+
def self.current(**) = Unshared::Scheduler.current(**)
|
|
34
|
+
|
|
35
|
+
# Starts a worker in this scope and retains the launch settings for other scopes.
|
|
36
|
+
# Additional options and the block are forwarded to {#initialize}.
|
|
37
|
+
# @param executor [Class, nil] Thread or a custom executor that stays in each Ractor.
|
|
38
|
+
# @param name [String, nil] Optional worker name.
|
|
39
|
+
# @param priority [Integer, nil] Optional worker thread priority.
|
|
40
|
+
# @yieldreturn [Object] Fiber scheduler to install in each worker.
|
|
41
|
+
# @return [Scheduler]
|
|
42
|
+
def self.create(executor = Thread, name: nil, priority: nil, **, &)
|
|
43
|
+
raise ArgumentError, "Local schedulers cannot launch Ractor workers" if executor && executor <= Ractor
|
|
44
|
+
worker = [executor || Thread, name && -String(name), priority && Integer(priority)].freeze
|
|
45
|
+
new(**, worker:, &)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Creates a handle whose scoped schedulers remain unstarted.
|
|
49
|
+
# The constructor block builds the fiber scheduler when a scope is launched or installed.
|
|
50
|
+
# @!macro scopes
|
|
51
|
+
# @overload initialize(capacity: 1024, backend: Farce.config.io_backend, scope: :ractor, &constructor)
|
|
52
|
+
# @param capacity [Integer, nil] Pending-task capacity, or nil for an unbounded queue in each scope.
|
|
53
|
+
# @param backend [Symbol] IO backend for the built-in scheduler.
|
|
54
|
+
# @param scope [Symbol] The scheduler scope. Defaults to :ractor.
|
|
55
|
+
# @yieldreturn [Object] Fiber scheduler to install in each scope.
|
|
56
|
+
def initialize(capacity: 1024, backend: Internal::FROZEN_CONFIG.io_backend, scope: :ractor,
|
|
57
|
+
worker: nil, &constructor)
|
|
58
|
+
@constructor = prepare_constructor(constructor || Internal::FROZEN_CONFIG.fiber_scheduler_constructor)
|
|
59
|
+
@backend = backend
|
|
60
|
+
@worker = worker
|
|
61
|
+
@schedulers = Strict::Map.new
|
|
62
|
+
@closed_all = Internal::Flag.new(false)
|
|
63
|
+
@initialization_key = Object.new.freeze
|
|
64
|
+
@registration_key = Object.new.freeze
|
|
65
|
+
super(scope:, capacity: capacity && Integer(capacity), backend:)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Enqueue a task in the current scope, preserving its block and arguments.
|
|
69
|
+
# `mode` is ignored when `auto_local` is true. Otherwise, only local and smart modes are accepted.
|
|
70
|
+
# @overload schedule(*args, mode: :smart, auto_local: true)
|
|
71
|
+
# @param (see Farce::Abstract::Executor#schedule)
|
|
72
|
+
# @return [self]
|
|
73
|
+
def schedule(...) = enqueue_task(false, ...)
|
|
74
|
+
|
|
75
|
+
# (see Abstract::Scheduler#try_schedule)
|
|
76
|
+
def try_schedule(...) = enqueue_task(true, ...)
|
|
77
|
+
|
|
78
|
+
# Starts a worker for the current scope. Other scopes retain their creation behavior.
|
|
79
|
+
# @param executor [Class, nil] Thread or a custom executor that stays in this Ractor.
|
|
80
|
+
# @param name [String, nil] Optional worker name.
|
|
81
|
+
# @param priority [Integer, nil] Optional worker thread priority.
|
|
82
|
+
# @return [Thread, Object] The worker.
|
|
83
|
+
# @raise [ArgumentError] If a Ractor executor is supplied.
|
|
84
|
+
def launch(executor = Thread, name: nil, priority: nil)
|
|
85
|
+
raise ArgumentError, "Local schedulers cannot launch Ractor workers" if executor && executor <= Ractor
|
|
86
|
+
raise "Scheduler may not be reused" if @closed_all.value
|
|
87
|
+
active_scoped_value.launch(executor, name:, priority:)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Closes the current scope and rejects new submissions there.
|
|
91
|
+
# @return [self]
|
|
92
|
+
def close
|
|
93
|
+
current_scheduler&.close
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Requests shutdown in every scope and Ractor and rejects future scopes.
|
|
98
|
+
# Does not wait for queued or running tasks to finish.
|
|
99
|
+
# @return [self]
|
|
100
|
+
def close_all
|
|
101
|
+
return self unless @closed_all.compare_and_set(false, true)
|
|
102
|
+
@schedulers.each_pair do |scheduler, queue|
|
|
103
|
+
queue.push([scheduler, :close].freeze, clock: 0)
|
|
104
|
+
rescue ClosedQueueError
|
|
105
|
+
# The owning Ractor may already have shut down its supervisor.
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
close
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @return [Symbol] The current scope's scheduler state.
|
|
112
|
+
def state = current_scheduler&.state || :closed
|
|
113
|
+
|
|
114
|
+
# @return [Exception, nil] The current scope's scheduler error.
|
|
115
|
+
def error = current_scheduler&.error
|
|
116
|
+
|
|
117
|
+
# @return [Boolean] Whether the current scope rejects new tasks.
|
|
118
|
+
def closed? = @closed_all.value || scoped_value.closed?
|
|
119
|
+
|
|
120
|
+
# @return [Boolean] Whether the current scope is dispatching tasks.
|
|
121
|
+
def alive? = current_scheduler&.alive? || false
|
|
122
|
+
|
|
123
|
+
# @return [Ractor] The current scope's owning Ractor.
|
|
124
|
+
def owner = Ractor.current
|
|
125
|
+
|
|
126
|
+
# @param value [Ractor] The current Ractor.
|
|
127
|
+
# @raise [ArgumentError] If another owner is supplied.
|
|
128
|
+
def owner=(value)
|
|
129
|
+
raise ArgumentError, "owner must be the current Ractor" unless value == Ractor.current
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @return [Boolean] Whether a fiber scheduler constructor was supplied.
|
|
133
|
+
def wraps_external? = !@constructor.nil?
|
|
134
|
+
|
|
135
|
+
# @return [Object, nil] The fiber scheduler in the current scope, when supplied by a constructor.
|
|
136
|
+
def external_scheduler = current_scheduler&.external_scheduler
|
|
137
|
+
|
|
138
|
+
# @api private
|
|
139
|
+
def inspect_with(inspector)
|
|
140
|
+
inspector.object_group(self) do
|
|
141
|
+
inspector.attribute(:scope, scope)
|
|
142
|
+
inspector.attribute(:state, state)
|
|
143
|
+
inspector.attribute(:backend, @backend)
|
|
144
|
+
yield if block_given?
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def enqueue_task(non_block, *, mode: :smart, auto_local: true, &block)
|
|
151
|
+
raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if @closed_all.value
|
|
152
|
+
scheduler = scoped_value
|
|
153
|
+
raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if scheduler.closed?
|
|
154
|
+
raise LocalJumpError, "Cannot schedule without a block" unless block
|
|
155
|
+
|
|
156
|
+
validate_local_mode(mode, auto_local)
|
|
157
|
+
register_scheduler(scheduler)
|
|
158
|
+
|
|
159
|
+
submitted =
|
|
160
|
+
if non_block
|
|
161
|
+
scheduler.try_schedule(*, mode:, auto_local:, &block)
|
|
162
|
+
else
|
|
163
|
+
scheduler.schedule(*, mode:, auto_local:, &block)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
submitted && self
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def current_scheduler
|
|
170
|
+
return scoped_value unless @closed_all.value
|
|
171
|
+
scheduler = Internal::Storage[self, scope:]
|
|
172
|
+
scheduler&.close
|
|
173
|
+
scheduler
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Worker setup can yield. Initialize once when fibers share a scope.
|
|
177
|
+
def scoped_value
|
|
178
|
+
storage = Internal::Storage.scope(scope)
|
|
179
|
+
scheduler = storage[self]
|
|
180
|
+
return scheduler if scheduler
|
|
181
|
+
|
|
182
|
+
initializers = Internal::Storage.store_if_absent(@initialization_key) { Internal::Storage::ThreadSafe.new }
|
|
183
|
+
initializers.store_if_absent(storage) { Internal::LeaseInitialization.new }.fetch { super }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def active_scoped_value
|
|
187
|
+
scheduler = scoped_value
|
|
188
|
+
register_scheduler(scheduler)
|
|
189
|
+
scheduler
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def register_scheduler(scheduler)
|
|
193
|
+
registrations = Internal::Storage.store_if_absent(@registration_key) { Internal::Storage::ThreadSafe.new }
|
|
194
|
+
registrations.store_if_absent(scheduler) do
|
|
195
|
+
queue, reference = Internal::ThreadPoolSupervisor.current.close_handle(scheduler)
|
|
196
|
+
@schedulers[reference] = queue
|
|
197
|
+
true
|
|
198
|
+
end
|
|
199
|
+
scheduler.close if @closed_all.value
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def new_scoped_value(**)
|
|
203
|
+
scheduler = Unshared::Scheduler.new(**, &@constructor)
|
|
204
|
+
if @worker && !@closed_all.value
|
|
205
|
+
register_scheduler(scheduler)
|
|
206
|
+
executor, name, priority = @worker
|
|
207
|
+
scheduler.launch(executor, name:, priority:) unless @closed_all.value
|
|
208
|
+
end
|
|
209
|
+
scheduler.close if @closed_all.value
|
|
210
|
+
scheduler
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def set_scheduler!(...)
|
|
214
|
+
raise "Scheduler may not be reused" if @closed_all.value
|
|
215
|
+
active_scoped_value.set_scheduler!(...)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|