farce 0.4.1-java → 0.5.0-java
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 +80 -2
- data/README.md +3 -1
- data/docs/benchmarks.md +37 -21
- data/docs/compatibility.md +13 -0
- data/docs/data-structures.md +37 -0
- data/docs/integrations.md +1 -0
- data/docs/introduction.md +19 -0
- data/docs/modes.md +2 -0
- data/docs/scheduling.md +35 -3
- data/docs/scopes.md +3 -1
- data/docs/shims.md +53 -0
- data/docs/utilities.md +31 -0
- data/docs/variants.md +9 -0
- data/lib/farce/abstract/concurrent_map.rb +122 -0
- data/lib/farce/abstract/counter.rb +35 -1
- data/lib/farce/abstract/executor.rb +30 -7
- data/lib/farce/abstract/flag.rb +12 -0
- data/lib/farce/abstract/pool.rb +158 -47
- data/lib/farce/abstract/priority_queue.rb +1 -1
- data/lib/farce/abstract/publisher.rb +189 -0
- data/lib/farce/abstract/queue.rb +29 -3
- data/lib/farce/abstract/scheduler.rb +41 -14
- data/lib/farce/abstract/timer_queue.rb +3 -3
- data/lib/farce/abstract/vector.rb +3 -1
- data/lib/farce/atom.rb +9 -2
- data/lib/farce/clock.rb +12 -0
- data/lib/farce/concurrent/count_down_latch.rb +37 -0
- data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
- data/lib/farce/concurrent/event.rb +60 -0
- data/lib/farce/concurrent/fiber_local_var.rb +31 -0
- data/lib/farce/concurrent/local_var.rb +65 -0
- data/lib/farce/concurrent/thread_local_var.rb +28 -0
- data/lib/farce/concurrent/tuple.rb +74 -0
- data/lib/farce/concurrent.rb +50 -0
- data/lib/farce/config.rb +112 -2
- data/lib/farce/counter.rb +0 -16
- data/lib/farce/engine/jruby/map.rb +8 -6
- data/lib/farce/engine/jruby/signal.rb +2 -2
- data/lib/farce/engine/jvm/farce.jar +0 -0
- data/lib/farce/engine/jvm/priority_queue.rb +14 -4
- data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
- data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
- data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
- data/lib/farce/engine/shared/exchanger.rb +2 -2
- data/lib/farce/engine/shared/queue.rb +43 -5
- data/lib/farce/engine/shared/ractor_selector.rb +1 -1
- data/lib/farce/engine/shared/strict_map.rb +5 -0
- data/lib/farce/engine/shared/unshared_atom.rb +1 -1
- data/lib/farce/engine/shared/unshared_vector.rb +8 -7
- data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
- data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
- data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
- data/lib/farce/engine/truffleruby/map.rb +8 -6
- data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
- data/lib/farce/engine/truffleruby/signal.rb +1 -1
- data/lib/farce/integrations/concurrent.rb +16 -3
- data/lib/farce/internal/blocking_priority_queue.rb +4 -9
- data/lib/farce/internal/flag_waiting.rb +1 -1
- data/lib/farce/internal/lease_initialization.rb +1 -1
- data/lib/farce/internal/lease_pool_state.rb +1 -1
- data/lib/farce/internal/lease_state.rb +2 -2
- data/lib/farce/internal/local_executor_methods.rb +3 -3
- data/lib/farce/internal/map_value_modes.rb +9 -2
- data/lib/farce/internal/pool_settings.rb +9 -0
- data/lib/farce/internal/pool_worker.rb +34 -21
- data/lib/farce/internal/reservation_waiting.rb +1 -1
- data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
- data/lib/farce/internal/weak_map_value_modes.rb +5 -0
- data/lib/farce/internal.rb +8 -8
- data/lib/farce/lazy.rb +3 -0
- data/lib/farce/lazy_ref.rb +3 -0
- data/lib/farce/local/lease.rb +2 -9
- data/lib/farce/local/lease_map.rb +2 -9
- data/lib/farce/local/pool.rb +38 -5
- data/lib/farce/local/publisher.rb +30 -0
- data/lib/farce/local/scheduler.rb +18 -5
- data/lib/farce/local/vector.rb +3 -2
- data/lib/farce/memoize.rb +314 -0
- data/lib/farce/pool.rb +5 -0
- data/lib/farce/priority_queue.rb +1 -1
- data/lib/farce/publisher.rb +57 -0
- data/lib/farce/signal.rb +1 -1
- data/lib/farce/strict/publisher.rb +31 -0
- data/lib/farce/strict/vector.rb +3 -2
- data/lib/farce/subscription/definitions.rb +27 -0
- data/lib/farce/subscription.rb +121 -0
- data/lib/farce/transaction/map.rb +35 -1
- data/lib/farce/unshared/pool.rb +1 -1
- data/lib/farce/unshared/publisher.rb +25 -0
- data/lib/farce/unshared/scheduler.rb +3 -3
- data/lib/farce/unshared/vector.rb +3 -2
- data/lib/farce/vector.rb +12 -9
- data/lib/farce/version.rb +1 -1
- data/lib/farce/walker/definitions.rb +13 -2
- data/lib/farce/weak_atom.rb +5 -2
- data/lib/farce.rb +3 -1
- metadata +19 -2
data/lib/farce/abstract/pool.rb
CHANGED
|
@@ -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
|
-
|
|
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 [
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 <
|
|
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,
|
|
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,
|
|
209
|
-
inspector.attribute(:max_size,
|
|
210
|
-
inspector.attribute(: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 <
|
|
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 =
|
|
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 =
|
|
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
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Abstract
|
|
7
|
+
# @abstract Shared superclass for all publisher implementations.
|
|
8
|
+
#
|
|
9
|
+
# Implements a publisher-subscriber (pub-sub) pattern.
|
|
10
|
+
# @see Farce::Publisher
|
|
11
|
+
class Publisher
|
|
12
|
+
include Internal::Freeze::Unfreezable
|
|
13
|
+
include Internal::Noncopyable
|
|
14
|
+
|
|
15
|
+
# @param subscribers [Array]
|
|
16
|
+
# the initial subscribers to add to the publisher, see {#subscribe}
|
|
17
|
+
def initialize(*subscribers)
|
|
18
|
+
@closed = Farce::Flag.new
|
|
19
|
+
super()
|
|
20
|
+
subscribers.each { subscribe(it) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Boolean] whether the publisher is closed.
|
|
24
|
+
def closed? = @closed.value
|
|
25
|
+
|
|
26
|
+
# Publishes the given payload to all current subscribers.
|
|
27
|
+
#
|
|
28
|
+
# Full bounded queues drop new messages.
|
|
29
|
+
# Failed publishing attempts to closed or frozen receivers cancel their subscriptions.
|
|
30
|
+
#
|
|
31
|
+
# @param payload [BasicObject] the payload to publish
|
|
32
|
+
# @return [self] the publisher itself
|
|
33
|
+
def publish(payload = nil)
|
|
34
|
+
raise "Cannot publish to a closed publisher" if closed?
|
|
35
|
+
map.each_value { it.publish(payload) }
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
alias send publish
|
|
39
|
+
alias << publish
|
|
40
|
+
alias broadcast publish
|
|
41
|
+
|
|
42
|
+
# @overload subscribe(subscriber)
|
|
43
|
+
# Adds a subscriber to the publisher.
|
|
44
|
+
# Register additional subscriber classes with {Subscription.define}.
|
|
45
|
+
# Publisher subscribers forward messages synchronously to their own subscribers.
|
|
46
|
+
# Avoid subscribing publishers in a cycle.
|
|
47
|
+
#
|
|
48
|
+
# @example Forward messages to another publisher
|
|
49
|
+
# upstream.subscribe(downstream)
|
|
50
|
+
# upstream.publish(:message) # also publishes to downstream's subscribers
|
|
51
|
+
#
|
|
52
|
+
# @param subscriber [BasicObject]
|
|
53
|
+
# The subscriber to add.
|
|
54
|
+
# Standard-library queues are supported by unshared and local publishers and retain the original payload.
|
|
55
|
+
# A full `Thread::SizedQueue` drops new messages. Publishing to a closed queue cancels its subscription.
|
|
56
|
+
#
|
|
57
|
+
# Calls one of the following methods:
|
|
58
|
+
# * {Ractor#send}
|
|
59
|
+
# * {Ractor::Port#send}
|
|
60
|
+
# * {Queue#try_push}
|
|
61
|
+
# * `Thread::Queue#push`
|
|
62
|
+
# * `Thread::SizedQueue#push` (nonblocking)
|
|
63
|
+
# * {Counter#increment} (without arguments)
|
|
64
|
+
# * {Flag#set}
|
|
65
|
+
# * {Atom#value=}
|
|
66
|
+
# * {Set#<<}
|
|
67
|
+
# * {Vector#<<}
|
|
68
|
+
# * {Signal#broadcast} (without arguments)
|
|
69
|
+
# * {Publisher#publish}
|
|
70
|
+
# * {Farce::Concurrent::Event#set} (without arguments)
|
|
71
|
+
# * {Farce::Concurrent::CountDownLatch#count_down} (without arguments)
|
|
72
|
+
# * `Proc#call`
|
|
73
|
+
#
|
|
74
|
+
# The concurrent-ruby integration supports these subscribers with unshared and local publishers:
|
|
75
|
+
# * `Concurrent::AtomicReference#value=`
|
|
76
|
+
# * `Concurrent::AtomicFixnum#increment` (without arguments)
|
|
77
|
+
# * `Concurrent::AtomicBoolean#make_true`
|
|
78
|
+
# * `Concurrent::Event#set` (without arguments)
|
|
79
|
+
# * `Concurrent::CountDownLatch#count_down` (without arguments)
|
|
80
|
+
#
|
|
81
|
+
# Set the default `mode`, `auto_local`, `capacity`, and similar options via initializers for these classes
|
|
82
|
+
# to influence how data transfer is handled.
|
|
83
|
+
#
|
|
84
|
+
# Support for additional subscribers can be added via {Subscription.define}.
|
|
85
|
+
#
|
|
86
|
+
# @overload subscribe(*args, mode: nil, scheduler: nil, capacity: nil)
|
|
87
|
+
# Adds a subscription callback.
|
|
88
|
+
# Messages run in order on the selected scheduler. A full queue drops new messages.
|
|
89
|
+
# Unsubscribing drains queued messages and stops the callback.
|
|
90
|
+
#
|
|
91
|
+
# @example
|
|
92
|
+
# counter = Farce::Counter.new
|
|
93
|
+
#
|
|
94
|
+
# publisher.subscribe(counter) do |counter, payload|
|
|
95
|
+
# counter.increment
|
|
96
|
+
# puts "Message ##{counter}: #{payload.inspect}"
|
|
97
|
+
# end
|
|
98
|
+
#
|
|
99
|
+
# @param args [Array] additional arguments to pass to the subscriber
|
|
100
|
+
# @param mode [Symbol, nil] The transfer mode to use
|
|
101
|
+
# @param scheduler [#schedule, nil] The scheduler to use for running the block
|
|
102
|
+
# @param capacity [Integer, nil] the maximum queued messages, or nil for an unbounded queue
|
|
103
|
+
# @yield [*args, payload] the block to be called with the given arguments and payload
|
|
104
|
+
# @yieldparam args [Array] the additional arguments passed to the block
|
|
105
|
+
# @yieldparam payload [Object] the payload sent via {#publish}
|
|
106
|
+
#
|
|
107
|
+
# @return [Subscription] the created subscription
|
|
108
|
+
def subscribe(...) = block_given? ? create_subscription(...) : add_subscription(...)
|
|
109
|
+
alias monitor subscribe
|
|
110
|
+
|
|
111
|
+
# @param subscriber [BasicObject] the subscriber to retrieve the subscription for
|
|
112
|
+
# @return [Subscription, nil] the subscription for the given subscriber, or nil if none exists.
|
|
113
|
+
def subscription_for(subscriber)
|
|
114
|
+
if subscriber.is_a?(Subscription)
|
|
115
|
+
return unless subscriber.publisher.equal?(self)
|
|
116
|
+
subscription = map[subscriber.key]
|
|
117
|
+
subscription if subscription.equal?(subscriber)
|
|
118
|
+
else
|
|
119
|
+
map[subscriber.__id__]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @return [Boolean] whether the given subscriber is currently subscribed.
|
|
124
|
+
def subscribed?(subscriber) = !!subscription_for(subscriber)&.subscribed?
|
|
125
|
+
|
|
126
|
+
# Unsubscribes the given subscriber from the publisher.
|
|
127
|
+
# @param subscriber [BasicObject] the subscriber to unsubscribe
|
|
128
|
+
# @return [void]
|
|
129
|
+
def unsubscribe(subscriber) = subscription_for(subscriber)&.unsubscribe
|
|
130
|
+
alias cancel unsubscribe
|
|
131
|
+
alias unmonitor unsubscribe
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def default_mode = :smart
|
|
136
|
+
def map = raise NoMethodError, "Subclasses must implement the private `map` method"
|
|
137
|
+
|
|
138
|
+
def add_subscription(subscriber)
|
|
139
|
+
if subscriber.is_a?(Subscription)
|
|
140
|
+
existing = subscription_for(subscriber)
|
|
141
|
+
return existing if existing
|
|
142
|
+
subscriber = subscriber.subscriber
|
|
143
|
+
end
|
|
144
|
+
return create_subscription(source: subscriber, &subscriber) if subscriber.is_a?(Proc)
|
|
145
|
+
|
|
146
|
+
map = self.map
|
|
147
|
+
map.store_if_absent(subscriber.__id__) { build_subscription(subscriber, map) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def create_subscription(*, capacity: nil, mode: nil, scheduler: nil, source: nil, &callback)
|
|
151
|
+
mode ||= default_mode
|
|
152
|
+
callback = Ractor.shareable_proc(&callback) unless mode == :local || Ractor.shareable?(callback)
|
|
153
|
+
scheduler ||= Farce
|
|
154
|
+
queue = Farce::Queue.new(capacity:, mode:)
|
|
155
|
+
map = self.map
|
|
156
|
+
created = false
|
|
157
|
+
subscription = map.store_if_absent((source || queue).__id__) do
|
|
158
|
+
created = true
|
|
159
|
+
build_subscription(queue, map, callback: true, source:)
|
|
160
|
+
end
|
|
161
|
+
return subscription unless created
|
|
162
|
+
|
|
163
|
+
scheduler.schedule(queue, callback, subscription, *, mode:) do |queue, callback, subscription, *args|
|
|
164
|
+
loop do
|
|
165
|
+
begin
|
|
166
|
+
payload = queue.pop
|
|
167
|
+
rescue ClosedQueueError
|
|
168
|
+
break
|
|
169
|
+
end
|
|
170
|
+
callback.call(*args, payload)
|
|
171
|
+
end
|
|
172
|
+
ensure
|
|
173
|
+
subscription.unsubscribe
|
|
174
|
+
queue.close
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
scheduled = true
|
|
178
|
+
subscription
|
|
179
|
+
ensure
|
|
180
|
+
unless scheduled
|
|
181
|
+
subscription&.unsubscribe if created
|
|
182
|
+
queue&.close
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def build_subscription(...) = Subscription.build(self, ...)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
data/lib/farce/abstract/queue.rb
CHANGED
|
@@ -11,8 +11,11 @@ module Farce
|
|
|
11
11
|
#
|
|
12
12
|
# Queues have a blocking and non-blocking interface.
|
|
13
13
|
#
|
|
14
|
-
# @!attribute [
|
|
15
|
-
#
|
|
14
|
+
# @!attribute [rw] capacity
|
|
15
|
+
# Assign a positive limit, or `nil` for an unbounded queue.
|
|
16
|
+
# Changes preserve queued values. Local queues change only the current scope.
|
|
17
|
+
# On JRuby and TruffleRuby, normal FIFO queues retain their initial bounded or unbounded choice.
|
|
18
|
+
# Switching between these states may raise `ArgumentError`. Priority and timer queues support both.
|
|
16
19
|
# @return [Integer, nil] The maximum number of items the queue can hold.
|
|
17
20
|
#
|
|
18
21
|
# @!attribute [r] num_waiting
|
|
@@ -173,10 +176,33 @@ module Farce
|
|
|
173
176
|
|
|
174
177
|
# Whether the queue is at capacity.
|
|
175
178
|
# @return [Boolean] `true` if the queue is at capacity, `false` otherwise.
|
|
176
|
-
def full?
|
|
179
|
+
def full?
|
|
180
|
+
limit = capacity
|
|
181
|
+
limit && size >= limit
|
|
182
|
+
end
|
|
177
183
|
|
|
178
184
|
def capacity = internal_queue.capacity
|
|
179
185
|
|
|
186
|
+
# Change the limit without discarding queued values. Returns immediately.
|
|
187
|
+
#
|
|
188
|
+
# New pushes wait while the queue is at or above the new limit.
|
|
189
|
+
# Increasing or removing the limit wakes blocked producers.
|
|
190
|
+
#
|
|
191
|
+
# Local queues change only the current scope. New scopes retain the constructor limit.
|
|
192
|
+
#
|
|
193
|
+
# Priority and timer queues support bounded and unbounded transitions on every engine.
|
|
194
|
+
#
|
|
195
|
+
# FIFO queues on JRuby and TruffleRuby retain their initial bounded or unbounded choice.
|
|
196
|
+
#
|
|
197
|
+
# @param value [Integer, nil] the positive capacity, or `nil` to remove the limit
|
|
198
|
+
# @return [Integer, nil] the new capacity
|
|
199
|
+
# @raise [ArgumentError] if the capacity is not positive
|
|
200
|
+
# @raise [ArgumentError] on JRuby and TruffleRuby, normal FIFO queues may reject transitions
|
|
201
|
+
# between bounded and unbounded states
|
|
202
|
+
def capacity=(value)
|
|
203
|
+
internal_queue.capacity = value.nil? ? nil : Integer(value)
|
|
204
|
+
end
|
|
205
|
+
|
|
180
206
|
def clear
|
|
181
207
|
internal_queue.clear
|
|
182
208
|
self
|