farce 0.2.0-arm-linux-gnu → 0.4.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 +91 -0
- data/README.md +117 -1451
- data/docs/compatibility.md +74 -0
- data/docs/concurrency.md +151 -0
- data/docs/data-structures.md +313 -0
- data/docs/installation.md +63 -0
- data/docs/integrations.md +124 -0
- data/docs/introduction.md +266 -0
- data/docs/modes.md +8 -5
- data/docs/scheduling.md +176 -0
- data/docs/scopes.md +3 -1
- data/docs/sharing.md +211 -0
- data/docs/shims.md +144 -0
- data/docs/utilities.md +93 -0
- data/docs/variants-and-scopes.md +92 -0
- data/docs/variants.md +18 -0
- data/lib/farce/abstract/executor.rb +130 -0
- data/lib/farce/abstract/flag.rb +6 -0
- data/lib/farce/abstract/pool.rb +334 -0
- data/lib/farce/abstract/scheduler.rb +539 -93
- data/lib/farce/backports.rb +274 -0
- data/lib/farce/engine/jruby/fiber_scheduler.rb +3 -0
- data/lib/farce/engine/jruby/scheduler_synchronization.rb +87 -0
- data/lib/farce/engine/jruby/signal.rb +7 -4
- data/lib/farce/engine/ruby/3.4/null_port.rb +21 -0
- data/lib/farce/engine/ruby/3.4/patch.rb +67 -0
- data/lib/farce/engine/ruby/3.4/port.rb +13 -12
- data/lib/farce/engine/ruby/3.4/ractor_methods.rb +0 -1
- data/lib/farce/engine/ruby/4.0/null_port.rb +36 -0
- data/lib/farce/engine/ruby/4.0/port.rb +1 -2
- data/lib/farce/engine/ruby/4.1/null_port.rb +5 -0
- data/lib/farce/engine/ruby.rb +7 -0
- data/lib/farce/engine/shared/main_scheduler.rb +2 -2
- data/lib/farce/engine/shared/null_port.rb +18 -0
- data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
- data/lib/farce/engine/shared/port.rb +41 -3
- data/lib/farce/engine/shared/proxy_owner.rb +1 -1
- data/lib/farce/engine/shared/queue.rb +4 -4
- data/lib/farce/engine/shared/ractor_methods.rb +2 -2
- data/lib/farce/engine/shared/ractor_selector.rb +73 -0
- data/lib/farce/engine/shared/unshared_atom.rb +9 -8
- data/lib/farce/engine/shared.rb +9 -0
- data/lib/farce/extension.rb +150 -0
- data/lib/farce/integrations/ractor_tmvar.rb +4 -1
- data/lib/farce/internal/fake_ractor.rb +10 -2
- 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/scheduler_lifecycle.rb +7 -4
- 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/null_port.rb +12 -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/system.rb +10 -2
- 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 +10 -18
- metadata +34 -2
|
@@ -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
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
# Special, write-only port that discards all messages.
|
|
7
|
+
# If the Ruby environment supports ports, this will be a real Port object.
|
|
8
|
+
# It can therefore be passed to any API expecting a `Ractor::Port`, including C code.
|
|
9
|
+
# Cannot be read from, but does not accumulate messages in memory.
|
|
10
|
+
# @return [Ractor::Port] Write-only port
|
|
11
|
+
NULL_PORT = Internal::NullPort.new
|
|
12
|
+
end
|
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::
|
|
40
|
-
include Internal::MarshalSupport::Reject
|
|
39
|
+
class Pool < Farce::Abstract::Pool
|
|
41
40
|
include Shareable::Unfreezable
|
|
42
|
-
include Internal::Inspect
|
|
43
41
|
|
|
44
|
-
|
|
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::
|
|
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::
|
|
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))
|