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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -2
  3. data/README.md +3 -1
  4. data/docs/benchmarks.md +37 -21
  5. data/docs/compatibility.md +13 -0
  6. data/docs/data-structures.md +37 -0
  7. data/docs/integrations.md +1 -0
  8. data/docs/introduction.md +19 -0
  9. data/docs/modes.md +2 -0
  10. data/docs/scheduling.md +35 -3
  11. data/docs/scopes.md +3 -1
  12. data/docs/shims.md +53 -0
  13. data/docs/utilities.md +31 -0
  14. data/docs/variants.md +9 -0
  15. data/lib/farce/abstract/concurrent_map.rb +122 -0
  16. data/lib/farce/abstract/counter.rb +35 -1
  17. data/lib/farce/abstract/executor.rb +30 -7
  18. data/lib/farce/abstract/flag.rb +12 -0
  19. data/lib/farce/abstract/pool.rb +158 -47
  20. data/lib/farce/abstract/priority_queue.rb +1 -1
  21. data/lib/farce/abstract/publisher.rb +189 -0
  22. data/lib/farce/abstract/queue.rb +29 -3
  23. data/lib/farce/abstract/scheduler.rb +41 -14
  24. data/lib/farce/abstract/timer_queue.rb +3 -3
  25. data/lib/farce/abstract/vector.rb +3 -1
  26. data/lib/farce/atom.rb +9 -2
  27. data/lib/farce/clock.rb +12 -0
  28. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  29. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  30. data/lib/farce/concurrent/event.rb +60 -0
  31. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  32. data/lib/farce/concurrent/local_var.rb +65 -0
  33. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  34. data/lib/farce/concurrent/tuple.rb +74 -0
  35. data/lib/farce/concurrent.rb +50 -0
  36. data/lib/farce/config.rb +112 -2
  37. data/lib/farce/counter.rb +0 -16
  38. data/lib/farce/engine/jruby/map.rb +8 -6
  39. data/lib/farce/engine/jruby/signal.rb +2 -2
  40. data/lib/farce/engine/jvm/farce.jar +0 -0
  41. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  42. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  43. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  44. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  45. data/lib/farce/engine/shared/exchanger.rb +2 -2
  46. data/lib/farce/engine/shared/queue.rb +43 -5
  47. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  48. data/lib/farce/engine/shared/strict_map.rb +5 -0
  49. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  50. data/lib/farce/engine/shared/unshared_vector.rb +8 -7
  51. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  52. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  53. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  54. data/lib/farce/engine/truffleruby/map.rb +8 -6
  55. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  56. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  57. data/lib/farce/integrations/concurrent.rb +16 -3
  58. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  59. data/lib/farce/internal/flag_waiting.rb +1 -1
  60. data/lib/farce/internal/lease_initialization.rb +1 -1
  61. data/lib/farce/internal/lease_pool_state.rb +1 -1
  62. data/lib/farce/internal/lease_state.rb +2 -2
  63. data/lib/farce/internal/local_executor_methods.rb +3 -3
  64. data/lib/farce/internal/map_value_modes.rb +9 -2
  65. data/lib/farce/internal/pool_settings.rb +9 -0
  66. data/lib/farce/internal/pool_worker.rb +34 -21
  67. data/lib/farce/internal/reservation_waiting.rb +1 -1
  68. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  69. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  70. data/lib/farce/internal.rb +8 -8
  71. data/lib/farce/lazy.rb +3 -0
  72. data/lib/farce/lazy_ref.rb +3 -0
  73. data/lib/farce/local/lease.rb +2 -9
  74. data/lib/farce/local/lease_map.rb +2 -9
  75. data/lib/farce/local/pool.rb +38 -5
  76. data/lib/farce/local/publisher.rb +30 -0
  77. data/lib/farce/local/scheduler.rb +18 -5
  78. data/lib/farce/local/vector.rb +3 -2
  79. data/lib/farce/memoize.rb +314 -0
  80. data/lib/farce/pool.rb +5 -0
  81. data/lib/farce/priority_queue.rb +1 -1
  82. data/lib/farce/publisher.rb +57 -0
  83. data/lib/farce/signal.rb +1 -1
  84. data/lib/farce/strict/publisher.rb +31 -0
  85. data/lib/farce/strict/vector.rb +3 -2
  86. data/lib/farce/subscription/definitions.rb +27 -0
  87. data/lib/farce/subscription.rb +121 -0
  88. data/lib/farce/transaction/map.rb +35 -1
  89. data/lib/farce/unshared/pool.rb +1 -1
  90. data/lib/farce/unshared/publisher.rb +25 -0
  91. data/lib/farce/unshared/scheduler.rb +3 -3
  92. data/lib/farce/unshared/vector.rb +3 -2
  93. data/lib/farce/vector.rb +12 -9
  94. data/lib/farce/version.rb +1 -1
  95. data/lib/farce/walker/definitions.rb +13 -2
  96. data/lib/farce/weak_atom.rb +5 -2
  97. data/lib/farce.rb +3 -1
  98. metadata +19 -2
@@ -28,7 +28,8 @@ module Farce
28
28
  end
29
29
 
30
30
  def eql?(other)
31
- result = other.is_a?(Key) && value.eql?(other.value)
31
+ result = other.is_a?(Key) &&
32
+ (BASIC_OBJECT_EQUAL_METHOD.bind_call(value, other.value) || value.eql?(other.value))
32
33
  owner = mutation_owner || (other.mutation_owner if other.is_a?(Key))
33
34
  Internal::Freeze.check(owner) if owner
34
35
  result
@@ -214,11 +215,11 @@ module Farce
214
215
  # Yield presence and value under the key reservation.
215
216
  # MAP_KEEP and MAP_DELETE are control results. Other results replace the value.
216
217
  # Return whether a change committed.
217
- def modify(key) # rubocop:disable Naming/PredicateMethod
218
+ def modify(key, timeout: nil)
218
219
  raise LocalJumpError, "no block given" unless block_given?
219
220
  check_frozen!
220
221
  wrapped = wrap_key(key)
221
- _, result = with_mutating_key_operation(wrapped, nil) do |reservation|
222
+ completed, result = with_mutating_key_operation(wrapped, timeout_deadline(timeout)) do |reservation|
222
223
  present, current = native_operation { [concurrent_key?(wrapped), concurrent_get(wrapped)] }
223
224
  value = yield(present, current)
224
225
  next false if MAP_KEEP.equal?(value)
@@ -231,7 +232,7 @@ module Farce
231
232
  changed!
232
233
  end
233
234
  end
234
- !!result
235
+ completed && !!result
235
236
  end
236
237
 
237
238
  def upsert(key, initial_value, timeout: nil)
@@ -557,7 +558,8 @@ module Farce
557
558
  native_operation { concurrent_get(wrapped) }
558
559
  end
559
560
  return fallback&.call unless completed
560
- ready = non_nil ? !current.nil? : !values_equal?(current, expected)
561
+ ready = non_nil ? !current.nil? :
562
+ Internal.value_changed?(current, expected, compare_values_by_identity?)
561
563
  return current if ready
562
564
  return fallback&.call unless wait_for_signal(@change_signal, generation, deadline)
563
565
  end
@@ -566,7 +568,7 @@ module Farce
566
568
  def wait_for_signal(signal, generation, deadline)
567
569
  return signal.wait(generation) unless deadline
568
570
 
569
- remaining = deadline - Clock.now
571
+ remaining = Clock.remaining(deadline)
570
572
  return false unless remaining.positive?
571
573
  !signal.wait(generation, timeout: remaining).nil?
572
574
  end
@@ -23,8 +23,8 @@ module Farce
23
23
  private_constant :Bucket, :TrackedItem
24
24
 
25
25
  class State
26
- attr_accessor :size, :sealed, :closed, :generation, :operation_owner
27
- attr_reader :buckets, :capacity, :signal, :lock, :tracked_items, :track_age
26
+ attr_accessor :size, :sealed, :closed, :generation, :operation_owner, :capacity
27
+ attr_reader :buckets, :signal, :lock, :tracked_items, :track_age
28
28
 
29
29
  def initialize(capacity:, signal:, lock:, track_age:)
30
30
  @buckets = []
@@ -139,7 +139,18 @@ module Farce
139
139
  with_queue_operation(state) { state.size.zero? } # rubocop:disable Style/ZeroLengthPredicate
140
140
  end
141
141
 
142
- def capacity = initialized_state.capacity
142
+ def capacity
143
+ state = initialized_state
144
+ with_queue_operation(state) { state.capacity }
145
+ end
146
+
147
+ def capacity=(value)
148
+ state = initialized_state
149
+ value = normalize_capacity(value)
150
+ with_queue_operation(state) do
151
+ notify_and_commit(state) { state.capacity = value }
152
+ end
153
+ end
143
154
 
144
155
  def clear
145
156
  state = initialized_state
@@ -262,7 +273,10 @@ module Farce
262
273
 
263
274
  def normalize_capacity(capacity)
264
275
  return nil if capacity.nil?
265
- capacity = capacity.to_int unless primitive_is_a?(capacity, Integer)
276
+ unless primitive_is_a?(capacity, Integer)
277
+ raise TypeError, "capacity must be an Integer or nil" unless capacity.respond_to?(:to_int)
278
+ capacity = capacity.to_int
279
+ end
266
280
  raise TypeError, "capacity must be an Integer or nil" unless primitive_is_a?(capacity, Integer)
267
281
  raise ArgumentError, "capacity must be positive or nil" unless capacity.positive?
268
282
  capacity
@@ -43,7 +43,7 @@ module Farce
43
43
  current = generation
44
44
  break current unless current == observed
45
45
 
46
- remaining = deadline - Clock.now if deadline
46
+ remaining = Clock.remaining(deadline)
47
47
  break TIMED_OUT if remaining && !remaining.positive?
48
48
  @num_waiting.set(@num_waiting.get + 1) unless waiting
49
49
  waiting = true
@@ -3,9 +3,14 @@
3
3
  # warn_indent: true
4
4
 
5
5
  # @!group concurrent-ruby Integration
6
- require "farce" unless defined?(Farce::Abstract)
7
- require "concurrent/map" unless defined?(Concurrent::Map)
8
- require "concurrent/tvar" unless defined?(Concurrent::TVar)
6
+ require "farce" unless defined?(Farce::Abstract)
7
+ require "concurrent/map" unless defined?(Concurrent::Map)
8
+ require "concurrent/tvar" unless defined?(Concurrent::TVar)
9
+ require "concurrent/atomic/atomic_reference" unless defined?(Concurrent::AtomicReference)
10
+ require "concurrent/atomic/atomic_fixnum" unless defined?(Concurrent::AtomicFixnum)
11
+ require "concurrent/atomic/atomic_boolean" unless defined?(Concurrent::AtomicBoolean)
12
+ require "concurrent/atomic/event" unless defined?(Concurrent::Event)
13
+ require "concurrent/atomic/count_down_latch" unless defined?(Concurrent::CountDownLatch)
9
14
 
10
15
  module Farce
11
16
  module Integrations
@@ -109,4 +114,12 @@ module Farce
109
114
  Internal::Converter.define(::Concurrent::Map, :Map) do |instance, value|
110
115
  value.each { instance[_1] = convert(_2) }
111
116
  end
117
+
118
+ Farce.on_main do
119
+ Subscription.define(::Concurrent::AtomicReference) { subscriber.value = it }
120
+ Subscription.define(::Concurrent::AtomicFixnum) { |_| subscriber.increment }
121
+ Subscription.define(::Concurrent::AtomicBoolean) { |_| subscriber.make_true }
122
+ Subscription.define(::Concurrent::Event) { |_| subscriber.set }
123
+ Subscription.define(::Concurrent::CountDownLatch) { |_| subscriber.count_down }
124
+ end
112
125
  end
@@ -32,7 +32,8 @@ module Farce
32
32
 
33
33
  wait(timeout) do
34
34
  raise_unwritable if sealed?
35
- size < capacity
35
+ limit = capacity
36
+ !limit || size < limit
36
37
  end
37
38
  end
38
39
 
@@ -76,7 +77,7 @@ module Farce
76
77
  while true
77
78
  return true if internal_queue.push(priority, value)
78
79
  raise ThreadError, "queue full" if non_block
79
- return false unless wait_push(timeout: remaining_timeout(deadline))
80
+ return false unless wait_push(timeout: Clock.remaining(deadline))
80
81
  end
81
82
  end
82
83
 
@@ -98,7 +99,7 @@ module Farce
98
99
  generation = internal_signal.generation
99
100
  raise ::Farce::ClosedQueueError, "queue is closed" if closed?
100
101
  return true if yield
101
- return false unless internal_signal.wait(generation, timeout: remaining_timeout(deadline))
102
+ return false unless internal_signal.wait(generation, timeout: Clock.remaining(deadline))
102
103
  end
103
104
  end
104
105
 
@@ -111,12 +112,6 @@ module Farce
111
112
  def timeout_at(timeout)
112
113
  Clock.timeout(normalize_timeout(timeout)) unless timeout.nil?
113
114
  end
114
-
115
- def remaining_timeout(timeout_at)
116
- return unless timeout_at
117
- timeout = timeout_at - Clock.now
118
- timeout.positive? ? timeout : 0
119
- end
120
115
  end
121
116
  end
122
117
  end
@@ -21,7 +21,7 @@ module Farce
21
21
  observed = signal.generation
22
22
  current = value
23
23
  return current unless expected == current
24
- break unless signal.wait(observed, timeout: Internal.remaining_timeout(deadline))
24
+ break unless signal.wait(observed, timeout: Clock.remaining(deadline))
25
25
  end
26
26
  yield if block_given?
27
27
  end
@@ -31,7 +31,7 @@ module Farce
31
31
  reject_recursive_initialization!
32
32
  return unless wait
33
33
 
34
- remaining = deadline - Clock.now if deadline
34
+ remaining = Clock.remaining(deadline)
35
35
  return if remaining && !remaining.positive?
36
36
  reject_unscheduled_fiber_wait!
37
37
  @signal.wait(observed, timeout: wait_interval(remaining))
@@ -108,7 +108,7 @@ module Farce
108
108
  return create_resource(factory) if reserve_creation
109
109
  return unless wait
110
110
 
111
- remaining = deadline - Clock.now if deadline
111
+ remaining = Clock.remaining(deadline)
112
112
  raise TimeoutError, "lease pool checkout timed out" if remaining && !remaining.positive?
113
113
  interval = LeaseWaiting.wait_interval(remaining)
114
114
  changed = Thread.handle_interrupt(Exception => :immediate) do
@@ -156,7 +156,7 @@ module Farce
156
156
  resource = try_acquire(kind)
157
157
  return resource if resource
158
158
 
159
- remaining = deadline - Clock.now if deadline
159
+ remaining = Clock.remaining(deadline)
160
160
  raise TimeoutError, "lease checkout timed out" if remaining && !remaining.positive?
161
161
  reject_unscheduled_fiber_wait!
162
162
  interval = wait_interval(remaining)
@@ -180,7 +180,7 @@ module Farce
180
180
  outcome = try_acquire_block(block)
181
181
  return outcome.first if outcome
182
182
 
183
- remaining = deadline - Clock.now if deadline
183
+ remaining = Clock.remaining(deadline)
184
184
  raise TimeoutError, "lease checkout timed out" if remaining && !remaining.positive?
185
185
  reject_unscheduled_fiber_wait!
186
186
  interval = wait_interval(remaining)
@@ -16,10 +16,10 @@ module Farce
16
16
  # Run a task directly, preserving its block, arguments, and calling fiber.
17
17
  # `mode` and `auto_local` follow the same validation as {#schedule}.
18
18
  # @param (see Abstract::Executor#execute)
19
- # @return [self]
20
- def execute(*, mode: :smart, auto_local: true, &)
19
+ # @return [self, nil]
20
+ def execute(*, mode: :smart, auto_local: true, timeout: nil, &)
21
21
  validate_local_mode(mode, auto_local)
22
- super(*, mode: :local, auto_local: true, &)
22
+ super(*, mode: :local, auto_local: true, timeout:, &)
23
23
  end
24
24
 
25
25
  private
@@ -143,18 +143,25 @@ module Farce
143
143
  end
144
144
  end
145
145
 
146
+ # (see Abstract::ConcurrentMap#wait_until)
147
+ def wait_until(key, timeout: nil)
148
+ raise LocalJumpError, "no block given" unless block_given?
149
+ result = Internal.wait_until(@map, key, timeout:) { |current| yield(unwrap_value(current)) }
150
+ unwrap_value(result)
151
+ end
152
+
146
153
  def wait_until_changed(key, expected, timeout: nil, &)
147
154
  prepared_key = @map.normalize_external_key(key)
148
155
  expected = wrap_comparison(expected)
149
156
  deadline = Internal.timeout_deadline(timeout)
150
157
  loop do
151
- current = @map.get_prepared(prepared_key, timeout: Internal.remaining_timeout(deadline)) { TIMED_OUT }
158
+ current = @map.get_prepared(prepared_key, timeout: Clock.remaining(deadline)) { TIMED_OUT }
152
159
  return unwrap_result(current, &) if TIMED_OUT.equal?(current)
153
160
  return unwrap_value(current) unless values_equal?(nil.equal?(current) ? NIL_VALUE : current, expected)
154
161
  result = @map.wait_until_changed_prepared(
155
162
  prepared_key,
156
163
  current,
157
- timeout: Internal.remaining_timeout(deadline),
164
+ timeout: Clock.remaining(deadline),
158
165
  ) { TIMED_OUT }
159
166
  return unwrap_result(result, &) if TIMED_OUT.equal?(result)
160
167
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Internal # :nodoc: all
7
+ PoolSettings = Data.define(:min_size, :max_size, :max_inflight, :grow_after, :shrink_after, :generation)
8
+ end
9
+ end
@@ -9,46 +9,59 @@ module Farce
9
9
  include Shareable::Unfreezable
10
10
 
11
11
  def initialize(pool)
12
- @pool = pool
13
- @inflight = Counter.new if pool.max_inflight || pool.shrink_after
14
- @pressure = pool.min_size < pool.max_size
12
+ @pool = pool
13
+ @inflight = Counter.new
15
14
  @registered = Atom.new(true)
16
15
  super()
17
16
  end
18
17
 
19
- def idle_timeout = @pool.shrink_after
20
-
21
- def wait_for_admission(scheduler, runnable_count)
22
- return false if @pool.drained?
23
- limit = @pool.max_inflight
24
- return true unless limit
18
+ def settings = @pool.settings
19
+ def idle_timeout = settings.shrink_after
25
20
 
21
+ def wait_for_admission(scheduler, runnable_count, settings = self.settings)
26
22
  loop do
27
23
  return false if @pool.drained?
28
- return true if @pool.closing? || @pool.at_max_size? || @inflight.value < limit
29
- return true if runnable_count && scheduler.public_send(runnable_count).zero?
24
+ return settings if @pool.closing?
25
+ count = @pool.size
26
+ limit = settings.max_inflight
27
+ return false if count > settings.max_size && retire?(excess: true)
28
+ # Busy workers keep queued dependencies runnable until they become idle.
29
+ return settings if !limit || count >= settings.max_size || @inflight.value < limit
30
+ return settings if runnable_count && scheduler.public_send(runnable_count).zero?
30
31
 
32
+ # Read the generation before rechecking state to avoid a lost configuration wakeup.
31
33
  generation = @pool.admission_generation
32
- next if @pool.closing? || @pool.at_max_size? || @inflight.value < limit
34
+ settings = self.settings
35
+ count = @pool.size
36
+ limit = settings.max_inflight
37
+ next if @pool.closing? || !limit || count >= settings.max_size || @inflight.value < limit
33
38
  @pool.wait_for_admission(generation)
39
+ settings = self.settings
34
40
  end
35
41
  end
36
42
 
37
- def task_started
38
- @inflight&.increment
39
- @pool.task_taken if @pressure
43
+ def task_started(settings = self.settings)
44
+ @inflight.increment
45
+ @pool.task_taken(settings)
40
46
  end
41
47
 
42
48
  def task_finished
43
- return unless @inflight
44
49
  @inflight.decrement
45
- @pool.admission_changed if @pool.max_inflight && !@pool.at_max_size?
50
+ settings = self.settings
51
+ count = @pool.size
52
+ if count > settings.max_size
53
+ @inflight.value.zero? ? @pool.wake_retiring_workers : @pool.admission_changed
54
+ elsif settings.max_inflight && count < settings.max_size
55
+ @pool.admission_changed
56
+ end
46
57
  end
47
58
 
48
- def retire?
49
- return false unless @inflight&.value&.zero?
50
- return false unless @pool.retire_worker
51
- @registered.compare_and_set(true, false)
59
+ def retire?(excess: false, expected: nil)
60
+ return false unless @inflight.value.zero?
61
+ return false unless @pool.retire_worker(excess:, expected:)
62
+ retired = @registered.compare_and_set(true, false)
63
+ @pool.worker_retired if retired
64
+ retired
52
65
  end
53
66
 
54
67
  def stopped(error, replace: true) # rubocop:disable Naming/PredicateMethod
@@ -9,7 +9,7 @@ module Farce
9
9
  module ReservationWaiting
10
10
  def self.wait(signal, generation, deadline)
11
11
  while true
12
- remaining = deadline - Clock.now if deadline
12
+ remaining = Clock.remaining(deadline)
13
13
  return false if remaining && !remaining.positive?
14
14
 
15
15
  # LeaseWaiting supplies the full remaining timeout on default engines
@@ -146,8 +146,8 @@ module Farce
146
146
  service_policy
147
147
  break if idle?
148
148
  deadline = @timers.peek_priority
149
- timeout = deadline ? deadline - Clock.now : nil
150
- timeout = 0 if timeout&.negative? || ready? || !@mailbox.empty?
149
+ timeout = Clock.remaining(deadline)
150
+ timeout = 0 if ready? || !@mailbox.empty?
151
151
  dispatch(timeout, @timers.empty? ? @dispatch_budget : 256)
152
152
  end
153
153
 
@@ -24,6 +24,11 @@ module Farce
24
24
  super
25
25
  end
26
26
 
27
+ def replace_if_exists(key, replacement, mode: nil, **)
28
+ @manager.validate_mode!(mode)
29
+ super
30
+ end
31
+
27
32
  def store_if_absent(key, mode: nil, **)
28
33
  raise LocalJumpError, "no block given" unless block_given?
29
34
  @manager.validate_mode!(mode)
@@ -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 remaining_timeout(deadline), deadline
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: remaining_timeout(deadline)) { return }
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
 
@@ -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)
@@ -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: remaining_timeout(deadline))
37
+ lease = scoped_lease(timeout: Clock.remaining(deadline))
38
38
  raise TimeoutError, "lease checkout timed out" unless lease
39
39
 
40
- timeout = remaining_timeout(deadline)
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: remaining_timeout(deadline))
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: remaining_timeout(deadline), receiver: self, missing_key:, &)
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
@@ -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. Tasks always keep their block and argument references. Only
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
- @constructor = prepare_constructor(constructor || Internal::FROZEN_CONFIG.fiber_scheduler_constructor)
35
- @pools = Strict::Map.new
36
- @closed_all = Internal::Flag.new(false)
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, :worker_stopped, :scale_if_needed)
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