farce 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +127 -1
- data/README.md +111 -17
- data/docs/gems/dry-types.md +8 -8
- data/docs/modes.md +75 -15
- data/docs/scopes.md +3 -1
- data/docs/variants.md +18 -0
- data/ext/farce/atom.c +37 -22
- data/ext/farce/bounded_map.c +17 -13
- data/ext/farce/containers.h +15 -0
- data/ext/farce/counter.c +13 -18
- data/ext/farce/exchanger.c +5 -1
- data/ext/farce/farce.c +1 -1
- data/ext/farce/flag.c +39 -3
- data/ext/farce/io.c +3 -3
- data/ext/farce/lock.c +17 -23
- data/ext/farce/map.c +88 -66
- data/ext/farce/priority_queue.c +55 -43
- data/ext/farce/queue.c +10 -4
- data/ext/farce/reactor.c +52 -25
- data/ext/farce/reactor.h +1 -1
- data/ext/farce/signal.c +5 -1
- data/ext/farce/transaction.c +20 -24
- data/ext/farce/tree_map.c +33 -18
- data/ext/farce/trie.c +81 -43
- data/ext/farce/unshareable.c +168 -19
- data/ext/farce/unshared_io_pool.h +21 -12
- data/ext/farce/unshared_signal.c +13 -2
- data/ext/farce/unshared_wait.h +3 -0
- data/ext/farce/vector.c +82 -18
- data/ext/farce/weak_map.c +75 -63
- data/lib/farce/_yard/macros.rb +18 -3
- data/lib/farce/abstract/executor.rb +130 -0
- data/lib/farce/abstract/flag.rb +73 -0
- data/lib/farce/abstract/pool.rb +334 -0
- data/lib/farce/abstract/queue.rb +4 -3
- data/lib/farce/abstract/scheduler.rb +539 -93
- data/lib/farce/abstract/set.rb +17 -5
- data/lib/farce/abstract/sorted_set.rb +2 -2
- data/lib/farce/abstract/timer_queue.rb +49 -10
- data/lib/farce/abstract/vector.rb +510 -28
- data/lib/farce/atom.rb +1 -1
- data/lib/farce/class_mirror.rb +20 -7
- data/lib/farce/config.rb +7 -1
- data/lib/farce/engine/jruby/signal.rb +7 -4
- data/lib/farce/engine/jvm/flag.rb +15 -3
- data/lib/farce/engine/ruby/shared/require_hook.rb +18 -0
- data/lib/farce/engine/ruby.rb +7 -0
- data/lib/farce/engine/shared/main_scheduler.rb +2 -2
- data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
- data/lib/farce/engine/shared/proxy_owner.rb +1 -1
- data/lib/farce/engine/shared/unshared_atom.rb +9 -8
- data/lib/farce/engine/shared/unshared_vector.rb +31 -4
- data/lib/farce/engine/shared/vector.rb +8 -0
- data/lib/farce/engine/shared/weak_map/index.rb +14 -1
- data/lib/farce/engine/shared.rb +9 -0
- data/lib/farce/engine/truffleruby/native/flag.rb +15 -3
- data/lib/farce/envelope.rb +54 -7
- data/lib/farce/exchanger.rb +1 -1
- data/lib/farce/flag.rb +2 -0
- data/lib/farce/integrations/active_support.rb +1 -1
- data/lib/farce/integrations/bson.rb +1 -1
- data/lib/farce/integrations/cbor.rb +1 -1
- data/lib/farce/integrations/concurrent.rb +3 -3
- data/lib/farce/integrations/date.rb +8 -0
- data/lib/farce/integrations/dry_types.rb +7 -4
- data/lib/farce/integrations/json.rb +2 -2
- data/lib/farce/integrations/msgpack.rb +1 -1
- data/lib/farce/integrations/oj.rb +3 -4
- data/lib/farce/integrations/psych.rb +1 -1
- data/lib/farce/integrations/ractor_sharing.rb +1 -1
- data/lib/farce/integrations/sorted_set.rb +1 -1
- data/lib/farce/integrations/weakref.rb +9 -1
- data/lib/farce/integrations/yajl.rb +3 -3
- data/lib/farce/integrations.rb +33 -23
- data/lib/farce/internal/flag_waiting.rb +30 -0
- data/lib/farce/internal/freeze.rb +8 -3
- 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/map_value_modes.rb +1 -1
- data/lib/farce/internal/scheduled_task.rb +1 -1
- data/lib/farce/internal/scheduler_lifecycle.rb +1 -1
- data/lib/farce/internal/thread_pool_supervisor.rb +45 -0
- data/lib/farce/internal/unshared_pool_worker.rb +14 -0
- data/lib/farce/internal.rb +14 -0
- data/lib/farce/lazy.rb +4 -4
- data/lib/farce/lazy_ref.rb +3 -3
- data/lib/farce/lfu_map.rb +1 -1
- data/lib/farce/local/flag.rb +4 -0
- data/lib/farce/local/pool.rb +111 -0
- data/lib/farce/local/scheduler.rb +219 -0
- data/lib/farce/local/scoped.rb +1 -1
- data/lib/farce/lru_map.rb +1 -1
- data/lib/farce/map.rb +2 -2
- data/lib/farce/mode_manager/smart_rule.rb +138 -0
- data/lib/farce/mode_manager.rb +12 -3
- data/lib/farce/molecule.rb +2 -2
- data/lib/farce/pool.rb +2 -290
- data/lib/farce/port.rb +4 -4
- data/lib/farce/priority_queue.rb +1 -1
- data/lib/farce/proxy/register.rb +6 -0
- data/lib/farce/proxy/supervisor.rb +1 -1
- data/lib/farce/proxy.rb +9 -4
- data/lib/farce/queue.rb +1 -1
- data/lib/farce/scheduler.rb +1 -502
- data/lib/farce/set.rb +1 -1
- data/lib/farce/sorted_set.rb +1 -1
- data/lib/farce/task.rb +285 -0
- data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
- data/lib/farce/timer_queue.rb +20 -6
- data/lib/farce/transaction/vector.rb +43 -9
- data/lib/farce/transaction/wrapper.rb +14 -0
- data/lib/farce/tree_map.rb +1 -1
- data/lib/farce/unsafe/lfu_map.rb +2 -0
- data/lib/farce/unsafe/lru_map.rb +2 -0
- data/lib/farce/unsafe/tree_map.rb +2 -0
- data/lib/farce/unshareable.rb +8 -19
- data/lib/farce/unshared/atom.rb +3 -0
- data/lib/farce/unshared/lfu_map.rb +2 -0
- data/lib/farce/unshared/lru_map.rb +2 -0
- data/lib/farce/unshared/map.rb +3 -0
- data/lib/farce/unshared/pool.rb +44 -0
- data/lib/farce/unshared/scheduler.rb +89 -0
- data/lib/farce/unshared/set.rb +1 -0
- data/lib/farce/unshared/sorted_set.rb +1 -0
- data/lib/farce/unshared/tree_map.rb +5 -1
- data/lib/farce/unshared/vector.rb +3 -0
- data/lib/farce/unshared/weak_atom.rb +5 -1
- data/lib/farce/unshared/weak_key_map.rb +3 -0
- data/lib/farce/unshared/weak_map.rb +3 -0
- data/lib/farce/unshared/weak_set.rb +1 -0
- data/lib/farce/unshared/weak_value_map.rb +3 -0
- data/lib/farce/vector.rb +44 -19
- data/lib/farce/version.rb +1 -1
- data/lib/farce.rb +20 -25
- metadata +17 -2
data/lib/farce/abstract/flag.rb
CHANGED
|
@@ -41,6 +41,14 @@ module Farce
|
|
|
41
41
|
# @return [Boolean] whether the replacement succeeded
|
|
42
42
|
# @raise [ArgumentError] if either argument is not true or false
|
|
43
43
|
#
|
|
44
|
+
# @!method wait_until_changed(expected, timeout: nil)
|
|
45
|
+
# Wait until the current value differs from the expected value.
|
|
46
|
+
# @param expected [Boolean] the value to wait to change from
|
|
47
|
+
# @param timeout [Numeric, nil] the total seconds available
|
|
48
|
+
# @yield called when the timeout expires
|
|
49
|
+
# @return [Boolean, BasicObject, nil] the changed value or timeout fallback
|
|
50
|
+
# @raise [ArgumentError] if expected is not true or false
|
|
51
|
+
#
|
|
44
52
|
# @!method toggle
|
|
45
53
|
# Atomically invert the value.
|
|
46
54
|
# @return [Boolean] the new value
|
|
@@ -51,6 +59,71 @@ module Farce
|
|
|
51
59
|
include Internal::ValueSerialization
|
|
52
60
|
include Internal::Inspect
|
|
53
61
|
|
|
62
|
+
# Wait until the flag becomes true.
|
|
63
|
+
# @param timeout [Numeric, nil] the total seconds available
|
|
64
|
+
# @yield called when the timeout expires
|
|
65
|
+
# @return [Boolean, BasicObject, nil] the changed value or timeout fallback
|
|
66
|
+
def wait_until_set(timeout: nil, &) = wait_until_changed(false, timeout:, &)
|
|
67
|
+
alias wait_until_true wait_until_set
|
|
68
|
+
alias wait_while_unset wait_until_set
|
|
69
|
+
alias wait_while_false wait_until_set
|
|
70
|
+
|
|
71
|
+
# Wait until the flag becomes false.
|
|
72
|
+
# @param timeout [Numeric, nil] the total seconds available
|
|
73
|
+
# @yield called when the timeout expires
|
|
74
|
+
# @return [Boolean, BasicObject, nil] the changed value or timeout fallback
|
|
75
|
+
def wait_until_unset(timeout: nil, &) = wait_until_changed(true, timeout:, &)
|
|
76
|
+
alias wait_until_false wait_until_unset
|
|
77
|
+
alias wait_while_set wait_until_unset
|
|
78
|
+
alias wait_while_true wait_until_unset
|
|
79
|
+
|
|
80
|
+
# Wait until a block condition matches the current value.
|
|
81
|
+
# One timeout budget covers all checks and waits. The block is not interrupted.
|
|
82
|
+
# @yieldparam value [Boolean] the current value
|
|
83
|
+
# @yieldreturn [Boolean] whether the value matches
|
|
84
|
+
# @param timeout [Numeric, nil] the total seconds available
|
|
85
|
+
# @return [Boolean, nil] the matching value, or nil on timeout
|
|
86
|
+
# @raise [LocalJumpError] if no block is given
|
|
87
|
+
def wait_until(timeout: nil, &) = Internal.wait_until(self, timeout:, &)
|
|
88
|
+
|
|
89
|
+
# Wait while the block returns a truthy value.
|
|
90
|
+
# @yieldparam value [Boolean] the current value
|
|
91
|
+
# @yieldreturn [BasicObject] a truthy value to keep waiting, or nil or false to stop
|
|
92
|
+
# @param timeout [Numeric, nil] the total seconds available
|
|
93
|
+
# @return [Boolean, nil] the value when the condition becomes false, or nil on timeout
|
|
94
|
+
# @raise [LocalJumpError] if no block is given
|
|
95
|
+
def wait_while(timeout: nil)
|
|
96
|
+
raise LocalJumpError, "no block given" unless block_given?
|
|
97
|
+
wait_until(timeout:) { |value| !yield(value) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Wait while `object === value` is true.
|
|
101
|
+
# @param object [#===] the pattern to stop matching
|
|
102
|
+
# @param timeout [Numeric, nil] the total seconds available
|
|
103
|
+
# @return [Boolean, nil] the first nonmatching value, or nil on timeout
|
|
104
|
+
def wait_while_match(object, timeout: nil)
|
|
105
|
+
wait_while(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# (see #wait_until_changed)
|
|
109
|
+
def wait_while_value(...) = wait_until_changed(...)
|
|
110
|
+
|
|
111
|
+
# Wait until `object == value` is true.
|
|
112
|
+
# @param object [BasicObject] the value to compare with the current value
|
|
113
|
+
# @param timeout [Numeric, nil] the total seconds available
|
|
114
|
+
# @return [Boolean, nil] the matching value, or nil on timeout
|
|
115
|
+
def wait_until_value(object, timeout: nil)
|
|
116
|
+
wait_until(timeout:) { |value| object == value }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Wait until `object === value` is true.
|
|
120
|
+
# @param object [#===] the pattern to match
|
|
121
|
+
# @param timeout [Numeric, nil] the total seconds available
|
|
122
|
+
# @return [Boolean, nil] the matching value, or nil on timeout
|
|
123
|
+
def wait_until_match(object, timeout: nil)
|
|
124
|
+
wait_until(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
|
|
125
|
+
end
|
|
126
|
+
|
|
54
127
|
# @api private
|
|
55
128
|
def inspect_with(inspector)
|
|
56
129
|
begin
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Abstract
|
|
7
|
+
# A task scheduler with a dynamically sized pool of workers.
|
|
8
|
+
#
|
|
9
|
+
# Pools accept work through {#schedule}. {#close} rejects new tasks and drains
|
|
10
|
+
# queued work. Concrete pools use either Ractor or Thread workers.
|
|
11
|
+
#
|
|
12
|
+
# @abstract Use {Farce::Pool}, {Unshared::Pool}, or {Local::Pool}.
|
|
13
|
+
class Pool < Executor
|
|
14
|
+
include Internal::Inspect
|
|
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
|
+
# Create and start an elastic pool using the configured fiber scheduler.
|
|
32
|
+
# An explicit constructor block overrides the configured default.
|
|
33
|
+
#
|
|
34
|
+
# `max_inflight` is a soft limit. A worker may exceed it when its scheduler
|
|
35
|
+
# has no runnable fibers or when the pool has reached `max_size`.
|
|
36
|
+
#
|
|
37
|
+
# @param min_size [Integer] Workers started immediately and retained while open.
|
|
38
|
+
# @param max_size [Integer] Maximum workers allowed.
|
|
39
|
+
# @param max_inflight [Integer, nil] Soft task limit for each worker.
|
|
40
|
+
# @param grow_after [Numeric] Queue wait before adding another worker.
|
|
41
|
+
# @param shrink_after [Numeric, nil] Idle time before an extra worker retires.
|
|
42
|
+
# @param capacity [Integer, nil] Pending-task capacity.
|
|
43
|
+
# @param backend [Symbol] IO backend for the built-in scheduler.
|
|
44
|
+
# @yieldreturn [Object] Fiber scheduler constructed in each worker.
|
|
45
|
+
def initialize(min_size: 0, max_size: 4, max_inflight: 64,
|
|
46
|
+
grow_after: 0.005, shrink_after: 30, capacity: 1024,
|
|
47
|
+
backend: Internal::FROZEN_CONFIG.io_backend, &constructor)
|
|
48
|
+
constructor ||= Internal::FROZEN_CONFIG.fiber_scheduler_constructor
|
|
49
|
+
initialize_configuration(min_size:, max_size:, max_inflight:, grow_after:, shrink_after:, backend:)
|
|
50
|
+
@constructor = prepare_constructor(constructor)
|
|
51
|
+
@queue = new_queue(capacity: capacity && Integer(capacity))
|
|
52
|
+
@state = Internal::Atom.new(:running)
|
|
53
|
+
@error = Internal::Atom.new
|
|
54
|
+
@worker_count = Internal::Atom.new(0)
|
|
55
|
+
@pressure = Internal::Atom.new
|
|
56
|
+
@admission = Internal::Signal.new
|
|
57
|
+
super()
|
|
58
|
+
publish_pool
|
|
59
|
+
begin
|
|
60
|
+
@min_size.times { start_worker }
|
|
61
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
62
|
+
close
|
|
63
|
+
raise
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Enqueue a task for any worker in the pool.
|
|
68
|
+
#
|
|
69
|
+
# Ractor pools reject local transfer. Thread pools preserve the block and
|
|
70
|
+
# arguments. With `auto_local: false`, they accept only local or smart modes.
|
|
71
|
+
#
|
|
72
|
+
# @overload schedule(*args, mode: :smart, auto_local: true)
|
|
73
|
+
# @!macro modes
|
|
74
|
+
# @param args [Array<Object>] Positional arguments passed to the task block.
|
|
75
|
+
# @param mode [Symbol] Argument transfer mode.
|
|
76
|
+
# @param auto_local [Boolean] Whether to use local transfer in a thread pool.
|
|
77
|
+
# @yield [*args] Task to execute.
|
|
78
|
+
# @return [Pool] self.
|
|
79
|
+
# @raise [PoolClosedError] If the pool is closing or closed.
|
|
80
|
+
def schedule(...) = enqueue_task(false, ...)
|
|
81
|
+
|
|
82
|
+
# Enqueues a task without waiting for queue space.
|
|
83
|
+
# Arguments and transfer modes follow {#schedule}.
|
|
84
|
+
#
|
|
85
|
+
# @overload try_schedule(*args, mode: :smart, auto_local: true)
|
|
86
|
+
# @param (see Farce::Abstract::Executor#schedule)
|
|
87
|
+
# @return [self, nil] self when submitted, or nil when the queue is full.
|
|
88
|
+
def try_schedule(...) = enqueue_task(true, ...)
|
|
89
|
+
|
|
90
|
+
# Stop accepting tasks and drain queued work.
|
|
91
|
+
#
|
|
92
|
+
# @return [Pool] self.
|
|
93
|
+
def close
|
|
94
|
+
return self if closed?
|
|
95
|
+
@state.compare_and_set(:running, :closing)
|
|
96
|
+
@queue.seal
|
|
97
|
+
admission_changed
|
|
98
|
+
if size.zero?
|
|
99
|
+
@queue.closed? ? finish_close : start_worker(drain: true)
|
|
100
|
+
end
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Return the first worker error, if any.
|
|
105
|
+
def error
|
|
106
|
+
value = @error.value
|
|
107
|
+
value.is_a?(Envelope) ? value.value : value
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Return the current number of running and starting workers.
|
|
111
|
+
def size = @worker_count.value
|
|
112
|
+
|
|
113
|
+
# Return the current pool state.
|
|
114
|
+
def state = @state.value
|
|
115
|
+
|
|
116
|
+
# Return whether the pool rejects new tasks.
|
|
117
|
+
def closed? = state != :running
|
|
118
|
+
|
|
119
|
+
# Return whether the pool is draining queued work.
|
|
120
|
+
def closing? = state == :closing
|
|
121
|
+
|
|
122
|
+
# Return whether closing workers have drained all admitted submissions.
|
|
123
|
+
# @api private
|
|
124
|
+
def drained? = closing? && @queue.closed?
|
|
125
|
+
|
|
126
|
+
# Return whether all configured workers have been started.
|
|
127
|
+
def at_max_size? = size >= @max_size
|
|
128
|
+
|
|
129
|
+
# Return the generation used by workers waiting for admission.
|
|
130
|
+
# @api private
|
|
131
|
+
def admission_generation = @admission.generation
|
|
132
|
+
|
|
133
|
+
# Wait for worker capacity or a pool state change.
|
|
134
|
+
# @api private
|
|
135
|
+
def wait_for_admission(generation) = @admission.wait(generation)
|
|
136
|
+
|
|
137
|
+
# Wake workers waiting at their soft admission limit.
|
|
138
|
+
# @api private
|
|
139
|
+
def admission_changed = @admission.broadcast
|
|
140
|
+
|
|
141
|
+
# Record that a task left the shared queue.
|
|
142
|
+
# @api private
|
|
143
|
+
def task_taken
|
|
144
|
+
return if @pressure.value.nil? || at_max_size?
|
|
145
|
+
return unless queue_empty?
|
|
146
|
+
@pressure.value = nil
|
|
147
|
+
arm_scaler if @queue.size.positive?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Reserve a worker retirement above the minimum size.
|
|
151
|
+
# @api private
|
|
152
|
+
def retire_worker
|
|
153
|
+
retired = false
|
|
154
|
+
@worker_count.update do |count|
|
|
155
|
+
if state == :running && count > @min_size && queue_empty?
|
|
156
|
+
retired = true
|
|
157
|
+
count - 1
|
|
158
|
+
else
|
|
159
|
+
count
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
retired
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Remove a stopped worker and preserve its failure.
|
|
166
|
+
# @api private
|
|
167
|
+
def worker_stopped(error, replace: true)
|
|
168
|
+
@worker_count.update { |count| count - 1 }
|
|
169
|
+
@error.compare_and_set(nil, Envelope.new(error)) if error
|
|
170
|
+
if closing?
|
|
171
|
+
if size.zero?
|
|
172
|
+
error && !drained? ? start_worker(drain: true) : finish_close
|
|
173
|
+
end
|
|
174
|
+
elsif state == :running && replace
|
|
175
|
+
begin
|
|
176
|
+
start_worker while size < @min_size
|
|
177
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
arm_scaler if @queue.size.positive?
|
|
181
|
+
end
|
|
182
|
+
admission_changed
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Recheck delayed queue pressure and grow by one worker.
|
|
186
|
+
# @api private
|
|
187
|
+
def scale_if_needed(started)
|
|
188
|
+
return unless state == :running && @pressure.value == started
|
|
189
|
+
age = @queue.oldest_age
|
|
190
|
+
return clear_pressure(started) unless age
|
|
191
|
+
return reschedule_scaler(started, @grow_after - age) if age < @grow_after
|
|
192
|
+
|
|
193
|
+
start_worker unless at_max_size?
|
|
194
|
+
return clear_pressure(started) if queue_empty? || at_max_size?
|
|
195
|
+
|
|
196
|
+
next_started = @queue.generation
|
|
197
|
+
schedule_scaler(next_started) if @pressure.compare_and_set(started, next_started)
|
|
198
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
199
|
+
@error.compare_and_set(nil, Envelope.new(e))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# @api private
|
|
203
|
+
def inspect_with(inspector)
|
|
204
|
+
super do
|
|
205
|
+
inspector.attribute(:state, state)
|
|
206
|
+
inspector.attribute(:backend, @backend)
|
|
207
|
+
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
|
|
211
|
+
yield if block_given?
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
def enqueue_task(non_block, *args, mode: :smart, auto_local: true, &block)
|
|
218
|
+
raise PoolClosedError, "cannot schedule task on a closed pool" if closed?
|
|
219
|
+
raise LocalJumpError, "Cannot schedule without a block" unless block
|
|
220
|
+
|
|
221
|
+
task = prepare_task(args, block, mode, auto_local)
|
|
222
|
+
return unless non_block ? @queue.try_push(task) : @queue.push(task)
|
|
223
|
+
arm_scaler
|
|
224
|
+
self
|
|
225
|
+
rescue ClosedQueueError
|
|
226
|
+
raise unless closed?
|
|
227
|
+
raise PoolClosedError, "cannot schedule task on a closed pool"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def initialize_configuration(min_size:, max_size:, max_inflight:, grow_after:, shrink_after:, backend:)
|
|
231
|
+
@min_size = Integer(min_size)
|
|
232
|
+
@max_size = Integer(max_size)
|
|
233
|
+
@max_inflight = max_inflight && Integer(max_inflight)
|
|
234
|
+
@grow_after = duration(grow_after, :grow_after)
|
|
235
|
+
@shrink_after = duration(shrink_after, :shrink_after, nil_allowed: true)
|
|
236
|
+
@backend = backend
|
|
237
|
+
validate_sizes
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def prepare_constructor(constructor)
|
|
241
|
+
return constructor if !constructor || Ractor.shareable?(constructor)
|
|
242
|
+
Ractor.shareable_proc(&constructor)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def prepare_task(args, block, mode, _auto_local)
|
|
246
|
+
raise ArgumentError, "local transfer is not supported by a pool" if mode == :local
|
|
247
|
+
Internal::ScheduledTask.new(args, block, mode)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def new_queue(capacity:) = Internal::Queue.new(capacity:, track_age: true)
|
|
251
|
+
def new_worker = Internal::PoolWorker.new(self)
|
|
252
|
+
def new_scheduler(**, &) = Farce::Scheduler.new(**, &)
|
|
253
|
+
def launch_scheduler(scheduler) = scheduler.launch_ractor
|
|
254
|
+
|
|
255
|
+
def validate_sizes
|
|
256
|
+
raise ArgumentError, "min_size must be non-negative" if @min_size.negative?
|
|
257
|
+
raise ArgumentError, "max_size must be positive" unless @max_size.positive?
|
|
258
|
+
raise ArgumentError, "min_size must not exceed max_size" if @min_size > @max_size
|
|
259
|
+
raise ArgumentError, "max_inflight must be positive" if @max_inflight && !@max_inflight.positive?
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def duration(value, name, nil_allowed: false)
|
|
263
|
+
return if value.nil? && nil_allowed
|
|
264
|
+
value = Float(value)
|
|
265
|
+
raise ArgumentError, "#{name} must be finite and non-negative" if !value.finite? || value.negative?
|
|
266
|
+
value
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def start_worker(drain: false, only_if_empty: false)
|
|
270
|
+
reserved = false
|
|
271
|
+
@worker_count.update do |count|
|
|
272
|
+
can_start = state == :running || (drain && closing? && !queue_empty?)
|
|
273
|
+
if can_start && count < @max_size && (!only_if_empty || count.zero?)
|
|
274
|
+
reserved = true
|
|
275
|
+
count + 1
|
|
276
|
+
else
|
|
277
|
+
count
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
return false unless reserved
|
|
281
|
+
|
|
282
|
+
worker = new_worker
|
|
283
|
+
scheduler = new_scheduler(
|
|
284
|
+
backend: @backend,
|
|
285
|
+
queue: @queue,
|
|
286
|
+
pool_worker: worker,
|
|
287
|
+
queue_owner: false,
|
|
288
|
+
&@constructor
|
|
289
|
+
)
|
|
290
|
+
launch_scheduler(scheduler)
|
|
291
|
+
admission_changed if at_max_size?
|
|
292
|
+
true
|
|
293
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
294
|
+
worker ? worker.stopped(e, replace: false) : @worker_count.update { |count| count - 1 }
|
|
295
|
+
raise
|
|
296
|
+
end
|
|
297
|
+
|
|
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?
|
|
302
|
+
|
|
303
|
+
started = @queue.generation
|
|
304
|
+
age = @queue.oldest_age
|
|
305
|
+
|
|
306
|
+
return unless age
|
|
307
|
+
return unless @pressure.compare_and_set(nil, started)
|
|
308
|
+
|
|
309
|
+
remaining = @grow_after - age
|
|
310
|
+
remaining = 0 if remaining.negative?
|
|
311
|
+
|
|
312
|
+
schedule_scaler(started, remaining)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def schedule_scaler(started, delay = @grow_after)
|
|
316
|
+
Internal::PoolSupervisor.schedule(self, started, delay)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def reschedule_scaler(started, delay)
|
|
320
|
+
next_started = @queue.generation
|
|
321
|
+
schedule_scaler(next_started, delay) if @pressure.compare_and_set(started, next_started)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def clear_pressure(started)
|
|
325
|
+
return unless @pressure.compare_and_set(started, nil)
|
|
326
|
+
arm_scaler if @queue.size.positive?
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def queue_empty? = @queue.size.zero? # rubocop:disable Style/ZeroLengthPredicate
|
|
330
|
+
|
|
331
|
+
def finish_close = @state.compare_and_set(:closing, :closed)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
data/lib/farce/abstract/queue.rb
CHANGED
|
@@ -98,7 +98,7 @@ module Farce
|
|
|
98
98
|
# @raise [Farce::ClosedQueueError] when the queue is closed
|
|
99
99
|
# @raise [Farce::SealedQueueError] when the queue is sealed but not yet closed
|
|
100
100
|
# @raise [ThreadError] when the queue is full and non_block is true
|
|
101
|
-
# @return [
|
|
101
|
+
# @return [BasicObject, false] a truthy object if the item was added, or `false` if the timeout expired
|
|
102
102
|
#
|
|
103
103
|
# @!method try_pop
|
|
104
104
|
# @abstract Subclasses may add additional parameters to this method.
|
|
@@ -116,8 +116,9 @@ module Farce
|
|
|
116
116
|
# @yield Block called if the queue is at capacity.
|
|
117
117
|
# @raise [Farce::ClosedQueueError] when the queue is closed
|
|
118
118
|
# @raise [Farce::SealedQueueError] when the queue is sealed but not yet closed
|
|
119
|
-
# @return [
|
|
120
|
-
#
|
|
119
|
+
# @return [BasicObject]
|
|
120
|
+
# A truthy object if the item was added, or the fallback result if the queue was at capacity.
|
|
121
|
+
# Returns `false` if the queue was at capacity and no block was given.
|
|
121
122
|
#
|
|
122
123
|
# @!method wait_pop(timeout: nil)
|
|
123
124
|
# @abstract Subclasses may add additional parameters to this method.
|