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
data/lib/farce/internal.rb
CHANGED
|
@@ -77,18 +77,12 @@ module Farce
|
|
|
77
77
|
Clock.now + timeout
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
def remaining_timeout(deadline)
|
|
81
|
-
return unless deadline
|
|
82
|
-
remaining = deadline - Clock.now
|
|
83
|
-
remaining.negative? ? 0 : remaining
|
|
84
|
-
end
|
|
85
|
-
|
|
86
80
|
# Repeat with one timeout budget. Even a zero timeout permits the first check.
|
|
87
81
|
def with_timeout(timeout)
|
|
88
82
|
raise LocalJumpError, "no block given" unless block_given?
|
|
89
83
|
deadline = timeout_deadline(timeout)
|
|
90
84
|
loop do
|
|
91
|
-
yield
|
|
85
|
+
yield Clock.remaining(deadline), deadline
|
|
92
86
|
return if deadline && Clock.now >= deadline
|
|
93
87
|
end
|
|
94
88
|
end
|
|
@@ -98,10 +92,16 @@ module Farce
|
|
|
98
92
|
with_timeout(timeout) do |_, deadline|
|
|
99
93
|
current = keys.empty? ? receiver.value : receiver[*keys]
|
|
100
94
|
return current if yield(current)
|
|
101
|
-
receiver.wait_until_changed(*keys, current, timeout:
|
|
95
|
+
receiver.wait_until_changed(*keys, current, timeout: Clock.remaining(deadline)) { return }
|
|
102
96
|
end
|
|
103
97
|
end
|
|
104
98
|
|
|
99
|
+
# An unchanged observation must permit waiting even when its equality is non-reflexive.
|
|
100
|
+
def value_changed?(current, expected, identity)
|
|
101
|
+
return false if BasicObject.instance_method(:equal?).bind_call(current, expected)
|
|
102
|
+
identity || !(current == expected) # rubocop:disable Style/InverseMethods -- != can be overridden independently.
|
|
103
|
+
end
|
|
104
|
+
|
|
105
105
|
def garbage_collectable?(object)
|
|
106
106
|
case object
|
|
107
107
|
when Integer, Float, Complex, Rational, Symbol, true, false, nil then false
|
data/lib/farce/lazy.rb
CHANGED
|
@@ -59,6 +59,9 @@ module Farce
|
|
|
59
59
|
# # Will only compute the expensive attribute once, and return the same value to all Ractors.
|
|
60
60
|
# 5.times { Ractor.new(object) { it.expensive_attribute } }
|
|
61
61
|
# ```
|
|
62
|
+
#
|
|
63
|
+
# @see Farce::Memoize
|
|
64
|
+
# @see Farce::LazyRef
|
|
62
65
|
class Lazy < Farce::Abstract::Lazy
|
|
63
66
|
include Shareable::Tracked
|
|
64
67
|
|
data/lib/farce/lazy_ref.rb
CHANGED
|
@@ -22,6 +22,9 @@ module Farce
|
|
|
22
22
|
# # initialized.value # => 0
|
|
23
23
|
# # lazy_ref[:key] = :value
|
|
24
24
|
# # initialized.value # => 1
|
|
25
|
+
# #
|
|
26
|
+
# # @see Farce::Memoize
|
|
27
|
+
# # @see Farce::Lazy
|
|
25
28
|
# class LazyRef < Farce::Reference
|
|
26
29
|
# # @!macro modes
|
|
27
30
|
# # @overload initialize(factory, mode: :smart)
|
data/lib/farce/local/lease.rb
CHANGED
|
@@ -34,10 +34,10 @@ module Farce
|
|
|
34
34
|
# (see Farce::Abstract::Lease#checkout)
|
|
35
35
|
def checkout(timeout: nil, &block)
|
|
36
36
|
deadline = timeout_deadline(timeout)
|
|
37
|
-
lease = scoped_lease(timeout:
|
|
37
|
+
lease = scoped_lease(timeout: Clock.remaining(deadline))
|
|
38
38
|
raise TimeoutError, "lease checkout timed out" unless lease
|
|
39
39
|
|
|
40
|
-
timeout =
|
|
40
|
+
timeout = Clock.remaining(deadline)
|
|
41
41
|
block ? lease.checkout(timeout:, &block) : lease.checkout(timeout:)
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -65,13 +65,6 @@ module Farce
|
|
|
65
65
|
end
|
|
66
66
|
Clock.now + timeout
|
|
67
67
|
end
|
|
68
|
-
|
|
69
|
-
def remaining_timeout(deadline)
|
|
70
|
-
return unless deadline
|
|
71
|
-
|
|
72
|
-
remaining = deadline - Clock.now
|
|
73
|
-
remaining.positive? ? remaining : 0
|
|
74
|
-
end
|
|
75
68
|
end
|
|
76
69
|
end
|
|
77
70
|
end
|
|
@@ -35,10 +35,10 @@ module Farce
|
|
|
35
35
|
|
|
36
36
|
def checkout_canonical(key, missing_key:, timeout: nil, &)
|
|
37
37
|
deadline = timeout_deadline(timeout)
|
|
38
|
-
map = scoped_lease_map(timeout:
|
|
38
|
+
map = scoped_lease_map(timeout: Clock.remaining(deadline))
|
|
39
39
|
raise TimeoutError, "lease checkout timed out" unless map
|
|
40
40
|
|
|
41
|
-
map.checkout(key, timeout:
|
|
41
|
+
map.checkout(key, timeout: Clock.remaining(deadline), receiver: self, missing_key:, &)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def try_checkout_canonical(key, missing_key:, &)
|
|
@@ -68,13 +68,6 @@ module Farce
|
|
|
68
68
|
end
|
|
69
69
|
Clock.now + timeout
|
|
70
70
|
end
|
|
71
|
-
|
|
72
|
-
def remaining_timeout(deadline)
|
|
73
|
-
return unless deadline
|
|
74
|
-
|
|
75
|
-
remaining = deadline - Clock.now
|
|
76
|
-
remaining.positive? ? remaining : 0
|
|
77
|
-
end
|
|
78
71
|
end
|
|
79
72
|
end
|
|
80
73
|
end
|
data/lib/farce/local/pool.rb
CHANGED
|
@@ -7,7 +7,8 @@ module Farce
|
|
|
7
7
|
# A shareable handle with an independent thread pool in each scope.
|
|
8
8
|
#
|
|
9
9
|
# Each pool uses the scaling settings and fiber scheduler configured at
|
|
10
|
-
# construction.
|
|
10
|
+
# construction. Changing settings tunes the current scope's pool. New scopes
|
|
11
|
+
# use the constructor settings. Tasks always keep their block and argument references. Only
|
|
11
12
|
# local and smart modes are accepted when `auto_local` is false. A scheduler
|
|
12
13
|
# constructor must be convertible to a shareable proc for use in each Ractor.
|
|
13
14
|
# {#close} drains only the current scope's pool. {#close_all} drains every pool
|
|
@@ -31,14 +32,45 @@ module Farce
|
|
|
31
32
|
grow_after: 0.005, shrink_after: 30, capacity: 1024, scope: :ractor,
|
|
32
33
|
backend: Internal::FROZEN_CONFIG.io_backend, &constructor)
|
|
33
34
|
initialize_configuration(min_size:, max_size:, max_inflight:, grow_after:, shrink_after:, backend:)
|
|
34
|
-
@
|
|
35
|
-
@
|
|
36
|
-
@
|
|
35
|
+
@capacity = capacity && Integer(capacity)
|
|
36
|
+
@constructor = prepare_constructor(constructor || Internal::FROZEN_CONFIG.fiber_scheduler_constructor)
|
|
37
|
+
@pools = Strict::Map.new
|
|
38
|
+
@closed_all = Internal::Flag.new(false)
|
|
37
39
|
@initialization_key = Object.new.freeze
|
|
38
40
|
super(scope:, min_size: @min_size, max_size: @max_size, max_inflight: @max_inflight,
|
|
39
41
|
grow_after: @grow_after, shrink_after: @shrink_after, capacity:, backend:)
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
# Settings changes affect only the current scope. New scopes use constructor defaults.
|
|
45
|
+
# @api private
|
|
46
|
+
def settings = (pool = current_pool) ? pool.settings : @settings.value
|
|
47
|
+
|
|
48
|
+
def capacity = (pool = current_pool) ? pool.capacity : @capacity
|
|
49
|
+
|
|
50
|
+
def min_size=(value)
|
|
51
|
+
scoped_value.min_size = value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def max_size=(value)
|
|
55
|
+
scoped_value.max_size = value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def max_inflight=(value)
|
|
59
|
+
scoped_value.max_inflight = value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def grow_after=(value)
|
|
63
|
+
scoped_value.grow_after = value
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def shrink_after=(value)
|
|
67
|
+
scoped_value.shrink_after = value
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def capacity=(value)
|
|
71
|
+
scoped_value.capacity = value
|
|
72
|
+
end
|
|
73
|
+
|
|
42
74
|
# Stop accepting work in this scope and drain its queued tasks.
|
|
43
75
|
# @return [self]
|
|
44
76
|
def close
|
|
@@ -74,7 +106,8 @@ module Farce
|
|
|
74
106
|
def closed? = @closed_all.value || super
|
|
75
107
|
|
|
76
108
|
Internal.delegate(self, :scoped_value, :drained?, :admission_generation, :wait_for_admission,
|
|
77
|
-
:admission_changed, :task_taken, :retire_worker, :
|
|
109
|
+
:admission_changed, :task_taken, :retire_worker, :wake_retiring_workers,
|
|
110
|
+
:worker_retired, :worker_stopped, :scale_if_needed)
|
|
78
111
|
|
|
79
112
|
private
|
|
80
113
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Local
|
|
7
|
+
# Local variant of {Farce::Publisher}.
|
|
8
|
+
class Publisher < Abstract::Publisher
|
|
9
|
+
include Shareable::Unfreezable
|
|
10
|
+
|
|
11
|
+
# The scope with independent contents.
|
|
12
|
+
# @return [Symbol] The configured scope name.
|
|
13
|
+
attr_reader :scope
|
|
14
|
+
|
|
15
|
+
# @overload initialize(*subscribers)
|
|
16
|
+
# (see Farce::Abstract#initialize)
|
|
17
|
+
def initialize(*, scope: :ractor)
|
|
18
|
+
raise ArgumentError, "Invalid scope: #{scope.inspect}" unless Farce::SCOPES.include?(scope)
|
|
19
|
+
@scope = scope
|
|
20
|
+
super(*)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def map = Internal::Storage.store_if_absent(self, scope:) { Unshared::Map.new }
|
|
26
|
+
|
|
27
|
+
def default_mode = :local
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -56,6 +56,7 @@ module Farce
|
|
|
56
56
|
def initialize(capacity: 1024, backend: Internal::FROZEN_CONFIG.io_backend, scope: :ractor,
|
|
57
57
|
worker: nil, &constructor)
|
|
58
58
|
@constructor = prepare_constructor(constructor || Internal::FROZEN_CONFIG.fiber_scheduler_constructor)
|
|
59
|
+
@capacity = capacity && Integer(capacity)
|
|
59
60
|
@backend = backend
|
|
60
61
|
@worker = worker
|
|
61
62
|
@schedulers = Strict::Map.new
|
|
@@ -65,15 +66,26 @@ module Farce
|
|
|
65
66
|
super(scope:, capacity: capacity && Integer(capacity), backend:)
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
# Pending-task capacity in this scope. New scopes retain the constructor value.
|
|
70
|
+
def capacity = (scheduler = current_scheduler) ? scheduler.capacity : @capacity
|
|
71
|
+
|
|
72
|
+
# Resize the queue in this scope without starting a worker.
|
|
73
|
+
def capacity=(value)
|
|
74
|
+
raise SchedulerClosedError, "cannot configure a closed scheduler" if @closed_all.value
|
|
75
|
+
scoped_value.capacity = value
|
|
76
|
+
end
|
|
77
|
+
|
|
68
78
|
# Enqueue a task in the current scope, preserving its block and arguments.
|
|
69
79
|
# `mode` is ignored when `auto_local` is true. Otherwise, only local and smart modes are accepted.
|
|
70
|
-
# @overload schedule(*args, mode: :smart, auto_local: true)
|
|
80
|
+
# @overload schedule(*args, mode: :smart, auto_local: true, timeout: nil)
|
|
71
81
|
# @param (see Farce::Abstract::Executor#schedule)
|
|
72
|
-
# @return [self]
|
|
82
|
+
# @return [self, nil] self when submitted, or nil when the scheduling timeout expires.
|
|
73
83
|
def schedule(...) = enqueue_task(false, ...)
|
|
74
84
|
|
|
75
85
|
# (see Abstract::Scheduler#try_schedule)
|
|
76
|
-
def try_schedule(
|
|
86
|
+
def try_schedule(*, mode: :smart, auto_local: true, &)
|
|
87
|
+
enqueue_task(true, *, mode:, auto_local:, &)
|
|
88
|
+
end
|
|
77
89
|
|
|
78
90
|
# Starts a worker for the current scope. Other scopes retain their creation behavior.
|
|
79
91
|
# @param executor [Class, nil] Thread or a custom executor that stays in this Ractor.
|
|
@@ -147,12 +159,13 @@ module Farce
|
|
|
147
159
|
|
|
148
160
|
private
|
|
149
161
|
|
|
150
|
-
def enqueue_task(non_block, *, mode: :smart, auto_local: true, &block)
|
|
162
|
+
def enqueue_task(non_block, *, mode: :smart, auto_local: true, timeout: nil, &block)
|
|
151
163
|
raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if @closed_all.value
|
|
152
164
|
scheduler = scoped_value
|
|
153
165
|
raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if scheduler.closed?
|
|
154
166
|
raise LocalJumpError, "Cannot schedule without a block" unless block
|
|
155
167
|
|
|
168
|
+
deadline = scheduling_deadline(timeout)
|
|
156
169
|
validate_local_mode(mode, auto_local)
|
|
157
170
|
register_scheduler(scheduler)
|
|
158
171
|
|
|
@@ -160,7 +173,7 @@ module Farce
|
|
|
160
173
|
if non_block
|
|
161
174
|
scheduler.try_schedule(*, mode:, auto_local:, &block)
|
|
162
175
|
else
|
|
163
|
-
scheduler.schedule(*, mode:, auto_local:, &block)
|
|
176
|
+
scheduler.schedule(*, mode:, auto_local:, timeout: Clock.remaining(deadline), &block)
|
|
164
177
|
end
|
|
165
178
|
|
|
166
179
|
submitted && self
|
data/lib/farce/local/vector.rb
CHANGED
|
@@ -8,10 +8,11 @@ module Farce
|
|
|
8
8
|
#
|
|
9
9
|
# @!method initialize(source = nil, compare_by_identity: false, scope: :ractor)
|
|
10
10
|
# @!macro scopes
|
|
11
|
-
# @param source [Array, nil]
|
|
11
|
+
# @param source [Array, Integer, nil] Initial values or a size filled with nil.
|
|
12
12
|
# @param compare_by_identity [Boolean] whether values are compared by identity
|
|
13
13
|
# @param scope [Symbol] the scope of the vector
|
|
14
14
|
# @return [Vector]
|
|
15
|
+
# @raise [ArgumentError] If the size is negative.
|
|
15
16
|
class Vector < Abstract::Vector
|
|
16
17
|
include Shareable::Tracked
|
|
17
18
|
include Scoped::Tracked
|
|
@@ -24,7 +25,7 @@ module Farce
|
|
|
24
25
|
self.class.new(values, compare_by_identity: compare_by_identity?, scope:)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
private def new_scoped_value(
|
|
28
|
+
private def new_scoped_value(source = nil, **) = Internal::UnsharedVector.new(normalize_vector_source(source), **)
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
end
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
# Ractor-friendly memoization module.
|
|
7
|
+
#
|
|
8
|
+
# Replaces the following pattern:
|
|
9
|
+
#
|
|
10
|
+
# ```ruby
|
|
11
|
+
# class MyClass
|
|
12
|
+
# def time
|
|
13
|
+
# @time ||= Time.now
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
# ```
|
|
17
|
+
#
|
|
18
|
+
# Using `Farce::Memoize` instead gives you:
|
|
19
|
+
#
|
|
20
|
+
# * Allows memoization after freezing
|
|
21
|
+
# * Is Ractor-safe (with optional scoping or transfer modes) and thread-safe
|
|
22
|
+
# * Adds one instance variable per memoized method (avoids hash lookup and doesn't bust Ruby's data structures)
|
|
23
|
+
# * Is Marshal-dump/load friendly
|
|
24
|
+
# * Allows weak memoization, where the memoized value is kept until it is garbage collected
|
|
25
|
+
# * Does not need to run code on the main Ractor for memoization
|
|
26
|
+
#
|
|
27
|
+
# Methods with arguments cannot be memoized. Explicitly use a {Farce::Abstract::Map map} instead.
|
|
28
|
+
#
|
|
29
|
+
# @example Using `memoize` method
|
|
30
|
+
# class MyClass
|
|
31
|
+
# include Farce::Memoize
|
|
32
|
+
# memoize def time = Time.now
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# instance = MyClass.new
|
|
36
|
+
# first_time = instance.time
|
|
37
|
+
# second_time = instance.time
|
|
38
|
+
#
|
|
39
|
+
# first_time == second_time # => true
|
|
40
|
+
# first_time == MyClass.new.time # => false
|
|
41
|
+
#
|
|
42
|
+
# @example Scope-specific memoization
|
|
43
|
+
# class MyClass
|
|
44
|
+
# include Farce::Memoize
|
|
45
|
+
#
|
|
46
|
+
# def first = expensive_computation(0)
|
|
47
|
+
# def last = expensive_computation(-1)
|
|
48
|
+
#
|
|
49
|
+
# # Memoize separately per Ractor
|
|
50
|
+
# memoize :first, :last, scope: :ractor
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
53
|
+
# @example Setting a mode
|
|
54
|
+
# class MyClass
|
|
55
|
+
# include Farce::Memoize
|
|
56
|
+
# memoize :expensive, mode: :make_shareable
|
|
57
|
+
# def expensive = expensive_computation
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# @example Without `memoize` extension
|
|
61
|
+
# class MyClass
|
|
62
|
+
# include Farce::Memoize[:time]
|
|
63
|
+
# def time = Time.now
|
|
64
|
+
# end
|
|
65
|
+
#
|
|
66
|
+
# @see Farce::Lazy
|
|
67
|
+
# @see Farce::LazyRef
|
|
68
|
+
#
|
|
69
|
+
# @!macro memoize_params
|
|
70
|
+
# @overload $0(*methods, scope: :global, mode: :smart, weak: false, visibility: nil)
|
|
71
|
+
# @param methods [Array<Symbol>]
|
|
72
|
+
# The methods to memoize
|
|
73
|
+
#
|
|
74
|
+
# @param scope [Symbol]
|
|
75
|
+
# The scope of the memoization.
|
|
76
|
+
# In addition to the {Farce::SCOPES normal scopes} it may also be set to `:global` or `:unshared`.
|
|
77
|
+
#
|
|
78
|
+
# @param mode [Symbol]
|
|
79
|
+
# The mode of the memoization. Ignored for any scope other than `:global`.
|
|
80
|
+
# Defaults to `:raise` with `weak: true`, which supports only `:raise`, `:make_shareable`, and `:dedup`.
|
|
81
|
+
#
|
|
82
|
+
# @param weak [Boolean]
|
|
83
|
+
# Whether to use weak memoization.
|
|
84
|
+
# If enabled, memoized values may be garbage collected when no longer referenced.
|
|
85
|
+
# In this case, next access to the memoized method may recompute the value.
|
|
86
|
+
#
|
|
87
|
+
# @param visibility [Symbol, nil]
|
|
88
|
+
# The visibility of the memoized methods.
|
|
89
|
+
# May be one of `:public`, `:protected`, or `:private`.
|
|
90
|
+
# If set to `nil`, {Farce::Memoize#memoize} will attempt to infer the visibility based on the context.
|
|
91
|
+
# Other methods will default to `:public`.
|
|
92
|
+
module Memoize
|
|
93
|
+
# A named module preserves the nil marker's identity across Marshal round trips.
|
|
94
|
+
NIL_VALUE = Module.new.freeze
|
|
95
|
+
private_constant :NIL_VALUE
|
|
96
|
+
|
|
97
|
+
# Mixins used to implement memoization for different methods, scopes, and modes
|
|
98
|
+
# @see Farce::Memoize
|
|
99
|
+
class Mixin < Module
|
|
100
|
+
KNOWN = Strict::Map.new
|
|
101
|
+
private_constant :KNOWN
|
|
102
|
+
|
|
103
|
+
# @!macro memoize_params
|
|
104
|
+
# Creates or retrieves a memoization mixin for the given methods and options.
|
|
105
|
+
# @return [Mixin] the memoization mixin
|
|
106
|
+
def self.new(*methods, **options)
|
|
107
|
+
return super unless self == Mixin
|
|
108
|
+
methods = Farce.dedup(methods.map(&:to_sym).uniq.sort)
|
|
109
|
+
options = Farce.dedup(normalize_options(options))
|
|
110
|
+
map = KNOWN.store_if_absent(options) { Strict::Map.new }
|
|
111
|
+
map.store_if_absent(methods) { super(methods, **options) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @api private
|
|
115
|
+
def self.normalize_options(options)
|
|
116
|
+
options[:visibility] &&= options[:visibility].to_sym
|
|
117
|
+
|
|
118
|
+
options[:scope] ||= :global
|
|
119
|
+
options[:visibility] ||= :public
|
|
120
|
+
options[:weak] ||= false
|
|
121
|
+
|
|
122
|
+
if options[:scope] == :global
|
|
123
|
+
options[:mode] ||= options[:weak] ? :raise : :smart
|
|
124
|
+
else
|
|
125
|
+
options.delete(:mode)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
options
|
|
129
|
+
end
|
|
130
|
+
private_class_method :normalize_options
|
|
131
|
+
|
|
132
|
+
# @return [Array<Symbol>] the memoized methods
|
|
133
|
+
attr_reader :memoized_methods
|
|
134
|
+
|
|
135
|
+
# @return [Symbol] the scope of the memoization
|
|
136
|
+
attr_reader :scope
|
|
137
|
+
|
|
138
|
+
# @return [Symbol, nil] the transfer mode, or nil for a scoped cache
|
|
139
|
+
attr_reader :mode
|
|
140
|
+
|
|
141
|
+
# @return [Symbol] the visibility of the memoized methods
|
|
142
|
+
attr_reader :visibility
|
|
143
|
+
|
|
144
|
+
# @api private
|
|
145
|
+
def initialize(methods, scope:, weak:, mode: nil, visibility: :public)
|
|
146
|
+
@memoized_methods = methods
|
|
147
|
+
@scope = scope
|
|
148
|
+
@mode = mode
|
|
149
|
+
@weak = !!weak # rubocop:disable Style/DoubleNegation
|
|
150
|
+
@visibility = visibility
|
|
151
|
+
|
|
152
|
+
super()
|
|
153
|
+
|
|
154
|
+
unless %i[public protected private].include?(visibility)
|
|
155
|
+
raise ArgumentError, "unsupported visibility: #{visibility.inspect}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
atom_class = weak ? "WeakAtom" : "Atom"
|
|
159
|
+
|
|
160
|
+
case scope
|
|
161
|
+
when :global
|
|
162
|
+
modes = weak ? Internal::WeakModeManager::MODES : MODES
|
|
163
|
+
raise ArgumentError, "unsupported mode: #{mode.inspect}" unless modes.include?(mode)
|
|
164
|
+
init = "::Farce::#{atom_class}.new(mode: #{mode.inspect})"
|
|
165
|
+
args = methods.join(", ")
|
|
166
|
+
args += ", mode: #{mode.inspect}" unless mode == :smart
|
|
167
|
+
when :unshared
|
|
168
|
+
init = "::Farce::Unshared::#{atom_class}.new"
|
|
169
|
+
args = "#{methods.join(", ")}, scope: :unshared"
|
|
170
|
+
when *SCOPES
|
|
171
|
+
init = "::Farce::Local::#{atom_class}.new(scope: #{scope.inspect})"
|
|
172
|
+
args = "#{methods.join(", ")}, scope: #{scope.inspect}"
|
|
173
|
+
else
|
|
174
|
+
raise ArgumentError, "unsupported scope: #{scope.inspect}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
set_temporary_name(-"Farce::Memoize[#{args}]") if instance_of?(Mixin)
|
|
178
|
+
initializer = +""
|
|
179
|
+
# Overlapping mixins need separate slots. Stable names also survive Marshal across processes.
|
|
180
|
+
slot = "#{methods.join(",").unpack1("H*")}_#{scope}_#{mode}_#{@weak}_#{visibility}"
|
|
181
|
+
|
|
182
|
+
methods.each do |method|
|
|
183
|
+
unless method =~ /\A[a-z_]\w*[!?]?\z/i && method != :initialize
|
|
184
|
+
raise ArgumentError, "unsuitable method name: #{method.inspect}"
|
|
185
|
+
end
|
|
186
|
+
ivar = "@_#{method.to_s.unpack1("H*")}_memoize_#{slot}_atom"
|
|
187
|
+
initializer << " #{ivar} = #{init}\n"
|
|
188
|
+
module_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
189
|
+
#{visibility} def #{method}
|
|
190
|
+
value = #{ivar}.store_if_absent do
|
|
191
|
+
result = super()
|
|
192
|
+
nil.equal?(result) ? NIL_VALUE : result
|
|
193
|
+
end
|
|
194
|
+
value unless NIL_VALUE.equal?(value)
|
|
195
|
+
end
|
|
196
|
+
RUBY
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Explicit forwarding preserves nested constructor keywords on JRuby.
|
|
200
|
+
module_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
201
|
+
def initialize(*, **, &)
|
|
202
|
+
#{initializer}
|
|
203
|
+
super
|
|
204
|
+
end
|
|
205
|
+
RUBY
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# @return [Boolean] whether the memoization is weak
|
|
209
|
+
def weak? = @weak
|
|
210
|
+
|
|
211
|
+
# allow `include Farce::Memoize[...]` to work like `prepend`
|
|
212
|
+
private def append_features(base) = base.prepend(self)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# @api private
|
|
216
|
+
def self.append_features(base) = base.extend(self)
|
|
217
|
+
private_class_method :append_features
|
|
218
|
+
|
|
219
|
+
# Creates or retrieves the memoization mixin for the given methods, scope, and mode
|
|
220
|
+
# @!macro memoize_params
|
|
221
|
+
# @return [Mixin] memoization mixin for the given methods, scope, and mode
|
|
222
|
+
def self.[](...) = Mixin.new(...)
|
|
223
|
+
|
|
224
|
+
# @!macro memoize_params
|
|
225
|
+
# Creates a memoization mixin for the given arguments and adds it to the current class.
|
|
226
|
+
#
|
|
227
|
+
# Can be used as a prefix to `def`:
|
|
228
|
+
#
|
|
229
|
+
# ```ruby
|
|
230
|
+
# memoize def some_method = expensive_computation
|
|
231
|
+
# ```
|
|
232
|
+
#
|
|
233
|
+
# Or called separately with the method names, useful for passing additional options:
|
|
234
|
+
#
|
|
235
|
+
# ```ruby
|
|
236
|
+
# def some_method = expensive_computation
|
|
237
|
+
#
|
|
238
|
+
# # memoize some_method per thread
|
|
239
|
+
# memoize :some_method, scope: :thread
|
|
240
|
+
# ```
|
|
241
|
+
#
|
|
242
|
+
# ## Visibility of Memoized Methods
|
|
243
|
+
#
|
|
244
|
+
# If no visibility is provided, it will determine the visibility based on the existing method definitions.
|
|
245
|
+
# If none exist, it will default to public. This happens when `memoize` is called.
|
|
246
|
+
# Make sure you set the visibility before calling this method.
|
|
247
|
+
#
|
|
248
|
+
# This works:
|
|
249
|
+
#
|
|
250
|
+
# ```ruby
|
|
251
|
+
# memoize private def some_method = expensive_computation
|
|
252
|
+
# ```
|
|
253
|
+
#
|
|
254
|
+
# As does this:
|
|
255
|
+
#
|
|
256
|
+
# ```ruby
|
|
257
|
+
# private
|
|
258
|
+
# memoize def some_method = expensive_computation
|
|
259
|
+
# ```
|
|
260
|
+
#
|
|
261
|
+
# Or this:
|
|
262
|
+
#
|
|
263
|
+
# ```ruby
|
|
264
|
+
# private def some_method = expensive_computation
|
|
265
|
+
# memoize :some_method
|
|
266
|
+
# ```
|
|
267
|
+
#
|
|
268
|
+
# But this doesn't, as the mixin's method will end up public:
|
|
269
|
+
#
|
|
270
|
+
# ```ruby
|
|
271
|
+
# private memoize def some_method = expensive_computation
|
|
272
|
+
# ```
|
|
273
|
+
#
|
|
274
|
+
# Same for this one, as the mixin's method will also end up public:
|
|
275
|
+
#
|
|
276
|
+
# ```ruby
|
|
277
|
+
# memoize :some_method
|
|
278
|
+
# private def some_method = expensive_computation
|
|
279
|
+
# ```
|
|
280
|
+
#
|
|
281
|
+
# You can of course set visibility explicitly when calling `memoize`:
|
|
282
|
+
#
|
|
283
|
+
# ```ruby
|
|
284
|
+
# memoize :some_method, visibility: :private
|
|
285
|
+
# ```
|
|
286
|
+
#
|
|
287
|
+
# @return [Array<Symbol>] the list of memoized methods
|
|
288
|
+
def memoize(*methods, visibility: nil, warn: true, **)
|
|
289
|
+
if visibility.nil?
|
|
290
|
+
groups = methods.group_by do |method|
|
|
291
|
+
next :protected if protected_method_defined?(method)
|
|
292
|
+
next :private if private_method_defined?(method)
|
|
293
|
+
:public
|
|
294
|
+
end
|
|
295
|
+
return groups.flat_map { memoize(*_2, visibility: _1, warn:, **) }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
if warn
|
|
299
|
+
methods.each do |name|
|
|
300
|
+
method = instance_method(name)
|
|
301
|
+
next if method.parameters.empty?
|
|
302
|
+
warn "#{method.owner}##{name} expects parameters and may be unsuitable for memoization"
|
|
303
|
+
rescue NameError => e
|
|
304
|
+
raise e unless e.name == name.to_sym
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
mixin = Farce::Memoize[*methods, visibility:, **]
|
|
309
|
+
prepend mixin
|
|
310
|
+
|
|
311
|
+
mixin.memoized_methods
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
data/lib/farce/pool.rb
CHANGED
|
@@ -11,6 +11,11 @@ module Farce
|
|
|
11
11
|
# `min_size` retire after `shrink_after` without queued or running work.
|
|
12
12
|
# By default, the pool starts with no workers and can shrink back to zero.
|
|
13
13
|
#
|
|
14
|
+
# Worker limits, task limits, scaling delays, and queue capacity can change while
|
|
15
|
+
# the pool runs. Lowering `max_size` retires extra workers as they become idle.
|
|
16
|
+
# Busy workers keep queued work runnable. Raising `min_size` starts workers
|
|
17
|
+
# immediately. Queued tasks are retained.
|
|
18
|
+
#
|
|
14
19
|
# Most importantly, it exposes a {#schedule} method compatible with {Scheduler#schedule}.
|
|
15
20
|
#
|
|
16
21
|
# @example
|
data/lib/farce/priority_queue.rb
CHANGED
|
@@ -42,7 +42,7 @@ module Farce
|
|
|
42
42
|
result = @reverse_order ? @queue.pop_last { empty = true } : @queue.pop { empty = true }
|
|
43
43
|
|
|
44
44
|
return @manager.unwrap(result) unless empty
|
|
45
|
-
remaining =
|
|
45
|
+
remaining = Clock.remaining(deadline)
|
|
46
46
|
return block_given? ? yield : nil unless wait_pop(timeout: remaining)
|
|
47
47
|
end
|
|
48
48
|
end
|