farce 0.2.0-x86_64-linux-gnu → 0.4.0-x86_64-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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +91 -0
  3. data/README.md +117 -1451
  4. data/docs/compatibility.md +74 -0
  5. data/docs/concurrency.md +151 -0
  6. data/docs/data-structures.md +313 -0
  7. data/docs/installation.md +63 -0
  8. data/docs/integrations.md +124 -0
  9. data/docs/introduction.md +266 -0
  10. data/docs/modes.md +8 -5
  11. data/docs/scheduling.md +176 -0
  12. data/docs/scopes.md +3 -1
  13. data/docs/sharing.md +211 -0
  14. data/docs/shims.md +144 -0
  15. data/docs/utilities.md +93 -0
  16. data/docs/variants-and-scopes.md +92 -0
  17. data/docs/variants.md +18 -0
  18. data/lib/farce/abstract/executor.rb +130 -0
  19. data/lib/farce/abstract/flag.rb +6 -0
  20. data/lib/farce/abstract/pool.rb +334 -0
  21. data/lib/farce/abstract/scheduler.rb +539 -93
  22. data/lib/farce/backports.rb +274 -0
  23. data/lib/farce/engine/jruby/fiber_scheduler.rb +3 -0
  24. data/lib/farce/engine/jruby/scheduler_synchronization.rb +87 -0
  25. data/lib/farce/engine/jruby/signal.rb +7 -4
  26. data/lib/farce/engine/ruby/3.4/null_port.rb +21 -0
  27. data/lib/farce/engine/ruby/3.4/patch.rb +67 -0
  28. data/lib/farce/engine/ruby/3.4/port.rb +13 -12
  29. data/lib/farce/engine/ruby/3.4/ractor_methods.rb +0 -1
  30. data/lib/farce/engine/ruby/4.0/null_port.rb +36 -0
  31. data/lib/farce/engine/ruby/4.0/port.rb +1 -2
  32. data/lib/farce/engine/ruby/4.1/null_port.rb +5 -0
  33. data/lib/farce/engine/ruby.rb +7 -0
  34. data/lib/farce/engine/shared/main_scheduler.rb +2 -2
  35. data/lib/farce/engine/shared/null_port.rb +18 -0
  36. data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
  37. data/lib/farce/engine/shared/port.rb +41 -3
  38. data/lib/farce/engine/shared/proxy_owner.rb +1 -1
  39. data/lib/farce/engine/shared/queue.rb +4 -4
  40. data/lib/farce/engine/shared/ractor_methods.rb +2 -2
  41. data/lib/farce/engine/shared/ractor_selector.rb +73 -0
  42. data/lib/farce/engine/shared/unshared_atom.rb +9 -8
  43. data/lib/farce/engine/shared.rb +9 -0
  44. data/lib/farce/extension.rb +150 -0
  45. data/lib/farce/integrations/ractor_tmvar.rb +4 -1
  46. data/lib/farce/internal/fake_ractor.rb +10 -2
  47. data/lib/farce/internal/lease_initialization.rb +1 -1
  48. data/lib/farce/internal/local_executor_methods.rb +33 -0
  49. data/lib/farce/internal/local_scheduled_task.rb +27 -0
  50. data/lib/farce/internal/scheduled_task.rb +1 -1
  51. data/lib/farce/internal/scheduler_lifecycle.rb +7 -4
  52. data/lib/farce/internal/thread_pool_supervisor.rb +45 -0
  53. data/lib/farce/internal/unshared_pool_worker.rb +14 -0
  54. data/lib/farce/internal.rb +14 -0
  55. data/lib/farce/local/pool.rb +111 -0
  56. data/lib/farce/local/scheduler.rb +219 -0
  57. data/lib/farce/null_port.rb +12 -0
  58. data/lib/farce/pool.rb +2 -290
  59. data/lib/farce/proxy.rb +2 -2
  60. data/lib/farce/scheduler.rb +1 -502
  61. data/lib/farce/system.rb +10 -2
  62. data/lib/farce/task.rb +285 -0
  63. data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
  64. data/lib/farce/unshared/pool.rb +44 -0
  65. data/lib/farce/unshared/scheduler.rb +89 -0
  66. data/lib/farce/version.rb +1 -1
  67. data/lib/farce.rb +10 -18
  68. 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::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))