farce 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -2
  3. data/README.md +3 -1
  4. data/docs/benchmarks.md +37 -21
  5. data/docs/compatibility.md +13 -0
  6. data/docs/data-structures.md +37 -0
  7. data/docs/integrations.md +1 -0
  8. data/docs/introduction.md +19 -0
  9. data/docs/modes.md +2 -0
  10. data/docs/scheduling.md +35 -3
  11. data/docs/scopes.md +3 -1
  12. data/docs/shims.md +53 -0
  13. data/docs/utilities.md +31 -0
  14. data/docs/variants.md +9 -0
  15. data/ext/farce/farce.c +9 -0
  16. data/ext/farce/map.c +14 -3
  17. data/ext/farce/priority_queue.c +46 -1
  18. data/ext/farce/queue.c +141 -7
  19. data/ext/farce/vector.c +3 -3
  20. data/ext/farce/weak_map.c +12 -3
  21. data/ext/java/org/farce/PriorityQueue.java +18 -2
  22. data/lib/farce/abstract/concurrent_map.rb +122 -0
  23. data/lib/farce/abstract/counter.rb +35 -1
  24. data/lib/farce/abstract/executor.rb +30 -7
  25. data/lib/farce/abstract/flag.rb +12 -0
  26. data/lib/farce/abstract/pool.rb +158 -47
  27. data/lib/farce/abstract/priority_queue.rb +1 -1
  28. data/lib/farce/abstract/publisher.rb +189 -0
  29. data/lib/farce/abstract/queue.rb +29 -3
  30. data/lib/farce/abstract/scheduler.rb +41 -14
  31. data/lib/farce/abstract/timer_queue.rb +3 -3
  32. data/lib/farce/abstract/vector.rb +3 -1
  33. data/lib/farce/atom.rb +9 -2
  34. data/lib/farce/clock.rb +12 -0
  35. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  36. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  37. data/lib/farce/concurrent/event.rb +60 -0
  38. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  39. data/lib/farce/concurrent/local_var.rb +65 -0
  40. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  41. data/lib/farce/concurrent/tuple.rb +74 -0
  42. data/lib/farce/concurrent.rb +50 -0
  43. data/lib/farce/config.rb +112 -2
  44. data/lib/farce/counter.rb +0 -16
  45. data/lib/farce/engine/jruby/map.rb +8 -6
  46. data/lib/farce/engine/jruby/signal.rb +2 -2
  47. data/lib/farce/engine/jvm/farce.jar +0 -0
  48. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  49. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  50. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  51. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  52. data/lib/farce/engine/shared/exchanger.rb +2 -2
  53. data/lib/farce/engine/shared/queue.rb +43 -5
  54. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  55. data/lib/farce/engine/shared/strict_map.rb +5 -0
  56. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  57. data/lib/farce/engine/shared/unshared_vector.rb +8 -7
  58. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  59. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  60. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  61. data/lib/farce/engine/truffleruby/map.rb +8 -6
  62. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  63. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  64. data/lib/farce/integrations/concurrent.rb +16 -3
  65. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  66. data/lib/farce/internal/flag_waiting.rb +1 -1
  67. data/lib/farce/internal/lease_initialization.rb +1 -1
  68. data/lib/farce/internal/lease_pool_state.rb +1 -1
  69. data/lib/farce/internal/lease_state.rb +2 -2
  70. data/lib/farce/internal/local_executor_methods.rb +3 -3
  71. data/lib/farce/internal/map_value_modes.rb +9 -2
  72. data/lib/farce/internal/pool_settings.rb +9 -0
  73. data/lib/farce/internal/pool_worker.rb +34 -21
  74. data/lib/farce/internal/reservation_waiting.rb +1 -1
  75. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  76. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  77. data/lib/farce/internal.rb +8 -8
  78. data/lib/farce/lazy.rb +3 -0
  79. data/lib/farce/lazy_ref.rb +3 -0
  80. data/lib/farce/local/lease.rb +2 -9
  81. data/lib/farce/local/lease_map.rb +2 -9
  82. data/lib/farce/local/pool.rb +38 -5
  83. data/lib/farce/local/publisher.rb +30 -0
  84. data/lib/farce/local/scheduler.rb +18 -5
  85. data/lib/farce/local/vector.rb +3 -2
  86. data/lib/farce/memoize.rb +314 -0
  87. data/lib/farce/pool.rb +5 -0
  88. data/lib/farce/priority_queue.rb +1 -1
  89. data/lib/farce/publisher.rb +57 -0
  90. data/lib/farce/signal.rb +1 -1
  91. data/lib/farce/strict/publisher.rb +31 -0
  92. data/lib/farce/strict/vector.rb +3 -2
  93. data/lib/farce/subscription/definitions.rb +27 -0
  94. data/lib/farce/subscription.rb +121 -0
  95. data/lib/farce/transaction/map.rb +35 -1
  96. data/lib/farce/unshared/pool.rb +1 -1
  97. data/lib/farce/unshared/publisher.rb +25 -0
  98. data/lib/farce/unshared/scheduler.rb +3 -3
  99. data/lib/farce/unshared/vector.rb +3 -2
  100. data/lib/farce/vector.rb +12 -9
  101. data/lib/farce/version.rb +1 -1
  102. data/lib/farce/walker/definitions.rb +13 -2
  103. data/lib/farce/weak_atom.rb +5 -2
  104. data/lib/farce.rb +3 -1
  105. metadata +18 -1
@@ -7,6 +7,22 @@ module Farce
7
7
  # @abstract Common numeric interface and atomic operations for counters.
8
8
  # @note This is a module rather than a class so {Farce::Counter} can inherit
9
9
  # directly from the native counter and avoid delegation overhead.
10
+ #
11
+ # @!method initial
12
+ # @return [Integer] The initial value retained by reset and copies.
13
+ #
14
+ # @!method value
15
+ # @return [Integer] The current value of the counter.
16
+ #
17
+ # @!method increment(by = 1)
18
+ # Increment the counter by the given amount (default is 1).
19
+ # @param [Numeric, String, #to_int] by The amount, converted to an Integer.
20
+ # @return [self] Returns self for chaining.
21
+ #
22
+ # @!method decrement(by = 1)
23
+ # Decrement the counter by the given amount (default is 1).
24
+ # @param [Numeric, String, #to_int] by The amount, converted to an Integer.
25
+ # @return [self] Returns self for chaining.
10
26
  module Counter
11
27
  include Internal::MarshalSupport::Counter
12
28
  include Internal::Copyable
@@ -24,6 +40,24 @@ module Farce
24
40
  include Value
25
41
  include Internal::ValueSerialization
26
42
 
43
+ # Pass the current value to the given block, replacing it with the block's result.
44
+ # May retry if the value changes during the block's execution.
45
+ #
46
+ # Provided for compatibility with `Concurrent::AtomicFixnum`.
47
+ #
48
+ # @yield [count] Calculate the new value based on the current value.
49
+ # @yieldparam count [Integer] the current value of the atomic fixnum.
50
+ # @yieldreturn [Integer] the new value to replace the current value.
51
+ # @return [Integer] the new value after the block has been applied.
52
+ def update
53
+ raise LocalJumpError, "no block given" unless block_given?
54
+ while true
55
+ current = value
56
+ new_value = yield(current)
57
+ return new_value if compare_and_set(current, new_value)
58
+ end
59
+ end
60
+
27
61
  # Wait until the current value differs from the expected value.
28
62
  # @param expected [Integer] the value to wait to change from
29
63
  # @param timeout [Numeric, nil] the total seconds available
@@ -35,7 +69,7 @@ module Farce
35
69
  observed = signal.generation
36
70
  current = value
37
71
  return current unless expected == current
38
- break unless signal.wait(observed, timeout: Internal.remaining_timeout(deadline))
72
+ break unless signal.wait(observed, timeout: Clock.remaining(deadline))
39
73
  end
40
74
  yield if block_given?
41
75
  end
@@ -12,7 +12,7 @@ module Farce
12
12
  #
13
13
  # @abstract Use a scheduler or pool class.
14
14
  #
15
- # @!method schedule(*args, mode: :smart, auto_local: true)
15
+ # @!method schedule(*args, mode: :smart, auto_local: true, timeout: nil)
16
16
  # @abstract This method must be implemented by concrete executor subclasses.
17
17
  #
18
18
  # Schedules a block for execution.
@@ -36,12 +36,18 @@ module Farce
36
36
  # Whether to automatically use a local mode when possible. Defaults to `true`.
37
37
  # May be ignored by some executors.
38
38
  #
39
+ # @param timeout [Numeric, nil]
40
+ # Maximum seconds to wait for ownership and queue space. Nil waits indefinitely.
41
+ # Zero only submits if no waiting is needed. Must be finite and non-negative.
42
+ # An expired attempt returns `nil` and does not enqueue the task.
43
+ # This does not limit task execution time. Executors without a queue may ignore it.
44
+ #
39
45
  # @yield [*args] The block to be executed by the scheduler.
40
46
  # @yieldparam args [Array] The arguments passed to the block.
41
47
  # @yieldreceiver [BasicObject, nil]
42
48
  # Local mode will not touch the block's binding (self stays the same), but other modes may change it to `nil`.
43
49
  #
44
- # @return [self]
50
+ # @return [self, nil] self when submitted, or nil when the scheduling timeout expires.
45
51
  #
46
52
  # @!method close
47
53
  # @abstract This method must be implemented by concrete executor subclasses.
@@ -84,7 +90,9 @@ module Farce
84
90
  # Attempts to submit a task. Queue-based executors return `nil` when full
85
91
  # instead of waiting for space. Other executors behave like {#schedule}.
86
92
  #
87
- # @param (see #schedule)
93
+ # @param args (see #schedule)
94
+ # @param mode (see #schedule)
95
+ # @param auto_local (see #schedule)
88
96
  # @return [self, nil] self when submitted, or nil when the queue is full.
89
97
  def try_schedule(...) = schedule(...)
90
98
 
@@ -98,14 +106,16 @@ module Farce
98
106
  # 1. If {#local?} returns true, and it would run in local mode (either due to `auto_local` or explicitly set
99
107
  # `mode: :local`), it executes the block directly without enqueuing it.
100
108
  # 2. It blocks until the block has been executed, either immediately in local mode or after being scheduled.
109
+ # A `timeout` only limits scheduling waits. Once submitted, this waits for completion without a timeout.
101
110
  #
102
111
  # @param (see #schedule)
103
- # @return [self]
104
- def execute(*, mode: :smart, auto_local: true, &callback)
112
+ # @return [self, nil] self after execution, or nil when the scheduling timeout expires.
113
+ def execute(*, mode: :smart, auto_local: true, timeout: nil, &callback)
105
114
  raise LocalJumpError, "Cannot yield without a block" unless block_given?
106
115
  raise SchedulerClosedError, "cannot execute task on a closed scheduler" if closed?
107
116
 
108
- if (auto_local || mode == :local) && local?
117
+ deadline = scheduling_deadline(timeout)
118
+ if (auto_local || mode == :local) && local?(wait: timeout.nil?)
109
119
  raise SchedulerClosedError, "cannot execute task on a closed scheduler" if closed?
110
120
  yield(*)
111
121
  return self
@@ -115,16 +125,29 @@ module Farce
115
125
  signal = Signal.new
116
126
  callback = Ractor.shareable_proc(&callback) unless Ractor.shareable?(callback)
117
127
 
118
- schedule(callback, ran, signal, *, mode:, auto_local:) do |callback, ran, signal, *args|
128
+ submitted = schedule(callback, ran, signal, *, mode:, auto_local:,
129
+ timeout: Clock.remaining(deadline)) do |callback, ran, signal, *args|
119
130
  callback.call(*args)
120
131
  ensure
121
132
  ran.set
122
133
  signal.broadcast
123
134
  end
135
+ return unless submitted
124
136
 
125
137
  signal.wait_until { ran.value }
126
138
  self
127
139
  end
140
+
141
+ private
142
+
143
+ def scheduling_deadline(timeout)
144
+ return if timeout.nil?
145
+ timeout = Float(timeout)
146
+ unless timeout.finite? && !timeout.negative?
147
+ raise ArgumentError, "timeout must be a finite, non-negative number or nil"
148
+ end
149
+ Clock.timeout(timeout)
150
+ end
128
151
  end
129
152
  end
130
153
  end
@@ -61,9 +61,21 @@ module Farce
61
61
 
62
62
  # @return [Boolean] whether the flag is set
63
63
  def set? = value
64
+ alias true? set?
64
65
 
65
66
  # @return [Boolean] whether the flag is unset
66
67
  def unset? = !value
68
+ alias false? unset?
69
+
70
+ # Explicitly sets the value to true.
71
+ # Provided for compatibility with `Concurrent::AtomicBoolean`.
72
+ # @return [Boolean] true if the value has changed
73
+ def make_true = compare_and_set(false, true)
74
+
75
+ # Explicitly sets the value to false.
76
+ # Provided for compatibility with `Concurrent::AtomicBoolean`.
77
+ # @return [Boolean] true if the value has changed
78
+ def make_false = compare_and_set(true, false)
67
79
 
68
80
  # Wait until the flag becomes true.
69
81
  # @param timeout [Numeric, nil] the total seconds available
@@ -13,21 +13,6 @@ module Farce
13
13
  class Pool < Executor
14
14
  include Internal::Inspect
15
15
 
16
- # @return [Float] Maximum queue wait before adding a worker.
17
- attr_reader :grow_after
18
-
19
- # @return [Integer, nil] Soft task limit for each worker.
20
- attr_reader :max_inflight
21
-
22
- # @return [Integer] Maximum worker count.
23
- attr_reader :max_size
24
-
25
- # @return [Integer] Minimum worker count.
26
- attr_reader :min_size
27
-
28
- # @return [Float, nil] Idle time before an extra worker retires.
29
- attr_reader :shrink_after
30
-
31
16
  # Create and start an elastic pool using the configured fiber scheduler.
32
17
  # An explicit constructor block overrides the configured default.
33
18
  #
@@ -49,6 +34,7 @@ module Farce
49
34
  initialize_configuration(min_size:, max_size:, max_inflight:, grow_after:, shrink_after:, backend:)
50
35
  @constructor = prepare_constructor(constructor)
51
36
  @queue = new_queue(capacity: capacity && Integer(capacity))
37
+ @queue.interrupt(settings, settings.generation)
52
38
  @state = Internal::Atom.new(:running)
53
39
  @error = Internal::Atom.new
54
40
  @worker_count = Internal::Atom.new(0)
@@ -57,25 +43,92 @@ module Farce
57
43
  super()
58
44
  publish_pool
59
45
  begin
60
- @min_size.times { start_worker }
46
+ settings.min_size.times { start_worker }
61
47
  rescue Exception # rubocop:disable Lint/RescueException
62
48
  close
63
49
  raise
64
50
  end
65
51
  end
66
52
 
53
+ # @return [Float] Maximum queue wait before adding a worker.
54
+ def grow_after = settings.grow_after
55
+
56
+ # @return [Integer, nil] Soft task limit for each worker.
57
+ def max_inflight = settings.max_inflight
58
+
59
+ # @return [Integer] Maximum worker count.
60
+ def max_size = settings.max_size
61
+
62
+ # @return [Integer] Minimum worker count.
63
+ def min_size = settings.min_size
64
+
65
+ # @return [Float, nil] Idle time before an extra worker retires.
66
+ def shrink_after = settings.shrink_after
67
+
68
+ # @api private
69
+ def settings = @settings.value
70
+
71
+ # Pending-task capacity. A smaller capacity preserves already queued tasks.
72
+ # @return [Integer, nil]
73
+ def capacity = @queue.capacity
74
+
75
+ # Resize the pending-task queue. Nil makes the queue unbounded on CRuby.
76
+ # JRuby and TruffleRuby retain their initial bounded or unbounded choice.
77
+ def capacity=(value)
78
+ @queue.capacity = value.nil? ? nil : Integer(value)
79
+ end
80
+
81
+ # Start and retain at least this many workers. Must not exceed max_size.
82
+ def min_size=(value)
83
+ value = Integer(value)
84
+ update_settings(min_size: value)
85
+ value
86
+ end
87
+
88
+ # Limit worker count. Excess workers retire when their running tasks leave them idle.
89
+ # Must be positive and at least min_size.
90
+ def max_size=(value)
91
+ value = Integer(value)
92
+ update_settings(max_size: value)
93
+ value
94
+ end
95
+
96
+ # Set the soft per-worker task limit. Nil disables the limit.
97
+ def max_inflight=(value)
98
+ value &&= Integer(value)
99
+ update_settings(max_inflight: value)
100
+ value
101
+ end
102
+
103
+ # Change the queue wait before adding a worker. Rechecks existing queued work.
104
+ def grow_after=(value)
105
+ value = duration(value, :grow_after)
106
+ update_settings(grow_after: value)
107
+ value
108
+ end
109
+
110
+ # Change the idle timeout. Nil disables idle retirement. Restarts idle waits.
111
+ def shrink_after=(value)
112
+ value = duration(value, :shrink_after, nil_allowed: true)
113
+ update_settings(shrink_after: value)
114
+ value
115
+ end
116
+
67
117
  # Enqueue a task for any worker in the pool.
68
118
  #
69
119
  # Ractor pools reject local transfer. Thread pools preserve the block and
70
120
  # arguments. With `auto_local: false`, they accept only local or smart modes.
121
+ # A `timeout` limits waiting for queue space. Expiry returns `nil` without
122
+ # enqueuing the task. Accepted tasks have no execution timeout.
71
123
  #
72
- # @overload schedule(*args, mode: :smart, auto_local: true)
124
+ # @overload schedule(*args, mode: :smart, auto_local: true, timeout: nil)
73
125
  # @!macro modes
74
126
  # @param args [Array<Object>] Positional arguments passed to the task block.
75
127
  # @param mode [Symbol] Argument transfer mode.
76
128
  # @param auto_local [Boolean] Whether to use local transfer in a thread pool.
129
+ # @param timeout [Numeric, nil] Maximum scheduling wait in seconds. Nil waits indefinitely, zero polls.
77
130
  # @yield [*args] Task to execute.
78
- # @return [Pool] self.
131
+ # @return [self, nil] self when submitted, or nil when the scheduling timeout expires.
79
132
  # @raise [PoolClosedError] If the pool is closing or closed.
80
133
  def schedule(...) = enqueue_task(false, ...)
81
134
 
@@ -83,9 +136,13 @@ module Farce
83
136
  # Arguments and transfer modes follow {#schedule}.
84
137
  #
85
138
  # @overload try_schedule(*args, mode: :smart, auto_local: true)
86
- # @param (see Farce::Abstract::Executor#schedule)
139
+ # @param args (see Farce::Abstract::Executor#schedule)
140
+ # @param mode (see Farce::Abstract::Executor#schedule)
141
+ # @param auto_local (see Farce::Abstract::Executor#schedule)
87
142
  # @return [self, nil] self when submitted, or nil when the queue is full.
88
- def try_schedule(...) = enqueue_task(true, ...)
143
+ def try_schedule(*, mode: :smart, auto_local: true, &)
144
+ enqueue_task(true, *, mode:, auto_local:, &)
145
+ end
89
146
 
90
147
  # Stop accepting tasks and drain queued work.
91
148
  #
@@ -124,7 +181,9 @@ module Farce
124
181
  def drained? = closing? && @queue.closed?
125
182
 
126
183
  # Return whether all configured workers have been started.
127
- def at_max_size? = size >= @max_size
184
+ # @overload at_max_size?
185
+ # @return [Boolean]
186
+ def at_max_size?(settings = self.settings) = size >= settings.max_size
128
187
 
129
188
  # Return the generation used by workers waiting for admission.
130
189
  # @api private
@@ -140,19 +199,22 @@ module Farce
140
199
 
141
200
  # Record that a task left the shared queue.
142
201
  # @api private
143
- def task_taken
144
- return if @pressure.value.nil? || at_max_size?
202
+ def task_taken(settings = self.settings)
203
+ return if @pressure.value.nil? || at_max_size?(settings)
145
204
  return unless queue_empty?
146
205
  @pressure.value = nil
147
- arm_scaler if @queue.size.positive?
206
+ arm_scaler(settings) if @queue.size.positive?
148
207
  end
149
208
 
150
209
  # Reserve a worker retirement above the minimum size.
151
210
  # @api private
152
- def retire_worker
211
+ def retire_worker(excess: false, expected: nil)
153
212
  retired = false
154
213
  @worker_count.update do |count|
155
- if state == :running && count > @min_size && queue_empty?
214
+ settings = self.settings
215
+ next count if expected && !expected.equal?(settings)
216
+ limit = excess ? settings.max_size : settings.min_size
217
+ if state == :running && count > limit && (excess || queue_empty?)
156
218
  retired = true
157
219
  count - 1
158
220
  else
@@ -162,9 +224,28 @@ module Farce
162
224
  retired
163
225
  end
164
226
 
227
+ # Recheck idle dispatchers after an excess worker finishes its last task.
228
+ # @api private
229
+ def wake_retiring_workers
230
+ return admission_changed if closed?
231
+ updated = @settings.update { |current| current.with(generation: current.generation + 1) }
232
+ @queue.interrupt(updated, updated.generation)
233
+ admission_changed
234
+ end
235
+
236
+ # Finish an already reserved retirement before attempting replacement.
237
+ # @api private
238
+ def worker_retired
239
+ ensure_minimum
240
+ admission_changed
241
+ rescue Exception => e # rubocop:disable Lint/RescueException
242
+ @error.compare_and_set(nil, Envelope.new(e))
243
+ end
244
+
165
245
  # Remove a stopped worker and preserve its failure.
166
246
  # @api private
167
247
  def worker_stopped(error, replace: true)
248
+ settings = self.settings
168
249
  @worker_count.update { |count| count - 1 }
169
250
  @error.compare_and_set(nil, Envelope.new(error)) if error
170
251
  if closing?
@@ -173,11 +254,11 @@ module Farce
173
254
  end
174
255
  elsif state == :running && replace
175
256
  begin
176
- start_worker while size < @min_size
257
+ start_worker(settings) while size < settings.min_size
177
258
  rescue Exception # rubocop:disable Lint/RescueException
178
259
  nil
179
260
  end
180
- arm_scaler if @queue.size.positive?
261
+ arm_scaler(settings) if @queue.size.positive?
181
262
  end
182
263
  admission_changed
183
264
  end
@@ -185,41 +266,44 @@ module Farce
185
266
  # Recheck delayed queue pressure and grow by one worker.
186
267
  # @api private
187
268
  def scale_if_needed(started)
269
+ settings = self.settings
188
270
  return unless state == :running && @pressure.value == started
189
271
  age = @queue.oldest_age
190
272
  return clear_pressure(started) unless age
191
- return reschedule_scaler(started, @grow_after - age) if age < @grow_after
273
+ return reschedule_scaler(started, settings.grow_after - age) if age < settings.grow_after
192
274
 
193
- start_worker unless at_max_size?
194
- return clear_pressure(started) if queue_empty? || at_max_size?
275
+ start_worker(settings) unless at_max_size?(settings)
276
+ return clear_pressure(started) if queue_empty? || at_max_size?(settings)
195
277
 
196
278
  next_started = @queue.generation
197
- schedule_scaler(next_started) if @pressure.compare_and_set(started, next_started)
279
+ schedule_scaler(next_started, settings.grow_after) if @pressure.compare_and_set(started, next_started)
198
280
  rescue Exception => e # rubocop:disable Lint/RescueException
199
281
  @error.compare_and_set(nil, Envelope.new(e))
200
282
  end
201
283
 
202
284
  # @api private
203
285
  def inspect_with(inspector)
286
+ settings = self.settings
204
287
  super do
205
288
  inspector.attribute(:state, state)
206
289
  inspector.attribute(:backend, @backend)
207
290
  inspector.attribute(:size, size)
208
- inspector.attribute(:min_size, @min_size)
209
- inspector.attribute(:max_size, @max_size)
210
- inspector.attribute(:max_inflight, @max_inflight) if @max_inflight
291
+ inspector.attribute(:min_size, settings.min_size)
292
+ inspector.attribute(:max_size, settings.max_size)
293
+ inspector.attribute(:max_inflight, settings.max_inflight) if settings.max_inflight
211
294
  yield if block_given?
212
295
  end
213
296
  end
214
297
 
215
298
  private
216
299
 
217
- def enqueue_task(non_block, *args, mode: :smart, auto_local: true, &block)
300
+ def enqueue_task(non_block, *args, mode: :smart, auto_local: true, timeout: nil, &block)
218
301
  raise PoolClosedError, "cannot schedule task on a closed pool" if closed?
219
302
  raise LocalJumpError, "Cannot schedule without a block" unless block
220
303
 
304
+ deadline = scheduling_deadline(timeout)
221
305
  task = prepare_task(args, block, mode, auto_local)
222
- return unless non_block ? @queue.try_push(task) : @queue.push(task)
306
+ return unless non_block ? @queue.try_push(task) : @queue.push(task, timeout: Clock.remaining(deadline))
223
307
  arm_scaler
224
308
  self
225
309
  rescue ClosedQueueError
@@ -227,6 +311,28 @@ module Farce
227
311
  raise PoolClosedError, "cannot schedule task on a closed pool"
228
312
  end
229
313
 
314
+ def update_settings(**changes)
315
+ raise PoolClosedError, "cannot configure a closed pool" if closed?
316
+ updated = @settings.update do |current|
317
+ value = current.with(**changes, generation: current.generation + 1)
318
+ raise ArgumentError, "min_size must be non-negative" if value.min_size.negative?
319
+ raise ArgumentError, "max_size must be positive" unless value.max_size.positive?
320
+ raise ArgumentError, "min_size must not exceed max_size" if value.min_size > value.max_size
321
+ raise ArgumentError, "max_inflight must be positive" if value.max_inflight && !value.max_inflight.positive?
322
+ value
323
+ end
324
+ @queue.interrupt(updated, updated.generation)
325
+ admission_changed
326
+ ensure_minimum
327
+ @pressure.value = nil
328
+ arm_scaler
329
+ updated
330
+ end
331
+
332
+ def ensure_minimum
333
+ start_worker while state == :running && size < settings.min_size
334
+ end
335
+
230
336
  def initialize_configuration(min_size:, max_size:, max_inflight:, grow_after:, shrink_after:, backend:)
231
337
  @min_size = Integer(min_size)
232
338
  @max_size = Integer(max_size)
@@ -235,6 +341,10 @@ module Farce
235
341
  @shrink_after = duration(shrink_after, :shrink_after, nil_allowed: true)
236
342
  @backend = backend
237
343
  validate_sizes
344
+ @settings = Internal::Atom.new(Internal::PoolSettings.new(
345
+ min_size: @min_size, max_size: @max_size, max_inflight: @max_inflight,
346
+ grow_after: @grow_after, shrink_after: @shrink_after, generation: 0,
347
+ ))
238
348
  end
239
349
 
240
350
  def prepare_constructor(constructor)
@@ -266,11 +376,11 @@ module Farce
266
376
  value
267
377
  end
268
378
 
269
- def start_worker(drain: false, only_if_empty: false)
379
+ def start_worker(settings = self.settings, drain: false, only_if_empty: false)
270
380
  reserved = false
271
381
  @worker_count.update do |count|
272
382
  can_start = state == :running || (drain && closing? && !queue_empty?)
273
- if can_start && count < @max_size && (!only_if_empty || count.zero?)
383
+ if can_start && count < settings.max_size && (!only_if_empty || count.zero?)
274
384
  reserved = true
275
385
  count + 1
276
386
  else
@@ -288,17 +398,17 @@ module Farce
288
398
  &@constructor
289
399
  )
290
400
  launch_scheduler(scheduler)
291
- admission_changed if at_max_size?
401
+ admission_changed if at_max_size?(settings)
292
402
  true
293
403
  rescue Exception => e # rubocop:disable Lint/RescueException
294
404
  worker ? worker.stopped(e, replace: false) : @worker_count.update { |count| count - 1 }
295
405
  raise
296
406
  end
297
407
 
298
- def arm_scaler
299
- return if at_max_size? || queue_empty? || state != :running
300
- start_worker(only_if_empty: true) if size.zero?
301
- return if at_max_size?
408
+ def arm_scaler(settings = self.settings)
409
+ return if at_max_size?(settings) || queue_empty? || state != :running
410
+ start_worker(settings, only_if_empty: true) if size.zero?
411
+ return if at_max_size?(settings)
302
412
 
303
413
  started = @queue.generation
304
414
  age = @queue.oldest_age
@@ -306,13 +416,13 @@ module Farce
306
416
  return unless age
307
417
  return unless @pressure.compare_and_set(nil, started)
308
418
 
309
- remaining = @grow_after - age
419
+ remaining = settings.grow_after - age
310
420
  remaining = 0 if remaining.negative?
311
421
 
312
422
  schedule_scaler(started, remaining)
313
423
  end
314
424
 
315
- def schedule_scaler(started, delay = @grow_after)
425
+ def schedule_scaler(started, delay = settings.grow_after)
316
426
  Internal::PoolSupervisor.schedule(self, started, delay)
317
427
  end
318
428
 
@@ -322,8 +432,9 @@ module Farce
322
432
  end
323
433
 
324
434
  def clear_pressure(started)
435
+ settings = self.settings
325
436
  return unless @pressure.compare_and_set(started, nil)
326
- arm_scaler if @queue.size.positive?
437
+ arm_scaler(settings) if @queue.size.positive?
327
438
  end
328
439
 
329
440
  def queue_empty? = @queue.size.zero? # rubocop:disable Style/ZeroLengthPredicate
@@ -44,7 +44,7 @@ module Farce
44
44
  end
45
45
 
46
46
  return result unless empty
47
- remaining = remaining_timeout(deadline)
47
+ remaining = Clock.remaining(deadline)
48
48
  return block_given? ? yield : nil unless wait_pop(timeout: remaining)
49
49
  end
50
50
  end