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.
- 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/ext/farce/farce.c +9 -0
- data/ext/farce/map.c +14 -3
- data/ext/farce/priority_queue.c +46 -1
- data/ext/farce/queue.c +141 -7
- data/ext/farce/vector.c +3 -3
- data/ext/farce/weak_map.c +12 -3
- data/ext/java/org/farce/PriorityQueue.java +18 -2
- 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 +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:
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/farce/abstract/flag.rb
CHANGED
|
@@ -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
|
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
|