farce 0.4.1-java → 0.6.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -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 +32 -0
  14. data/docs/variants.md +9 -0
  15. data/lib/farce/abstract/atom.rb +5 -1
  16. data/lib/farce/abstract/concurrent_map.rb +122 -0
  17. data/lib/farce/abstract/counter.rb +36 -1
  18. data/lib/farce/abstract/executor.rb +30 -7
  19. data/lib/farce/abstract/flag.rb +12 -0
  20. data/lib/farce/abstract/molecule.rb +14 -14
  21. data/lib/farce/abstract/pool.rb +158 -47
  22. data/lib/farce/abstract/priority_queue.rb +1 -1
  23. data/lib/farce/abstract/publisher.rb +189 -0
  24. data/lib/farce/abstract/queue.rb +29 -3
  25. data/lib/farce/abstract/scheduler.rb +41 -14
  26. data/lib/farce/abstract/timer_queue.rb +3 -3
  27. data/lib/farce/abstract/vector.rb +3 -1
  28. data/lib/farce/atom.rb +15 -2
  29. data/lib/farce/clock.rb +20 -7
  30. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  31. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  32. data/lib/farce/concurrent/event.rb +60 -0
  33. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  34. data/lib/farce/concurrent/local_var.rb +65 -0
  35. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  36. data/lib/farce/concurrent/tuple.rb +74 -0
  37. data/lib/farce/concurrent.rb +50 -0
  38. data/lib/farce/config.rb +112 -2
  39. data/lib/farce/counter.rb +5 -15
  40. data/lib/farce/deduper.rb +12 -4
  41. data/lib/farce/engine/jruby/fiber_scheduler.jar +0 -0
  42. data/lib/farce/engine/jruby/map.rb +8 -6
  43. data/lib/farce/engine/jruby/signal.rb +2 -2
  44. data/lib/farce/engine/jvm/counter.rb +0 -2
  45. data/lib/farce/engine/jvm/farce.jar +0 -0
  46. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  47. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  48. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  49. data/lib/farce/engine/ruby/shared/weak_cache_mirror.rb +56 -0
  50. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  51. data/lib/farce/engine/ruby.rb +5 -1
  52. data/lib/farce/engine/shared/exchanger.rb +2 -2
  53. data/lib/farce/engine/shared/queue.rb +43 -5
  54. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  55. data/lib/farce/engine/shared/strict_map.rb +5 -0
  56. data/lib/farce/engine/shared/trie/ignore_case.rb +136 -0
  57. data/lib/farce/engine/shared/trie.rb +14 -1
  58. data/lib/farce/engine/shared/trie_builder.rb +19 -8
  59. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  60. data/lib/farce/engine/shared/unshared_vector.rb +11 -8
  61. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  62. data/lib/farce/engine/shared/vector.rb +2 -0
  63. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  64. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  65. data/lib/farce/engine/shared.rb +2 -0
  66. data/lib/farce/engine/truffleruby/map.rb +8 -6
  67. data/lib/farce/engine/truffleruby/native/counter.rb +0 -2
  68. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  69. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  70. data/lib/farce/engine/truffleruby.rb +9 -0
  71. data/lib/farce/extension.rb +5 -3
  72. data/lib/farce/integrations/concurrent.rb +16 -3
  73. data/lib/farce/integrations/sorted_set.rb +1 -0
  74. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  75. data/lib/farce/internal/fake_ractor.rb +1 -1
  76. data/lib/farce/internal/flag_waiting.rb +1 -1
  77. data/lib/farce/internal/freeze.rb +1 -5
  78. data/lib/farce/internal/lease_initialization.rb +1 -1
  79. data/lib/farce/internal/lease_pool_state.rb +1 -1
  80. data/lib/farce/internal/lease_state.rb +2 -2
  81. data/lib/farce/internal/local_executor_methods.rb +3 -3
  82. data/lib/farce/internal/map_value_modes.rb +17 -9
  83. data/lib/farce/internal/pool_settings.rb +9 -0
  84. data/lib/farce/internal/pool_worker.rb +34 -21
  85. data/lib/farce/internal/reservation_waiting.rb +1 -1
  86. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  87. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  88. data/lib/farce/internal.rb +19 -8
  89. data/lib/farce/lazy.rb +3 -0
  90. data/lib/farce/lazy_ref.rb +3 -0
  91. data/lib/farce/local/counter.rb +4 -10
  92. data/lib/farce/local/lease.rb +2 -9
  93. data/lib/farce/local/lease_map.rb +2 -9
  94. data/lib/farce/local/pool.rb +38 -5
  95. data/lib/farce/local/publisher.rb +30 -0
  96. data/lib/farce/local/scheduler.rb +18 -5
  97. data/lib/farce/local/vector.rb +3 -2
  98. data/lib/farce/map.rb +8 -1
  99. data/lib/farce/memoize.rb +319 -0
  100. data/lib/farce/pool.rb +5 -0
  101. data/lib/farce/priority_queue.rb +1 -1
  102. data/lib/farce/publisher.rb +57 -0
  103. data/lib/farce/shareable.rb +3 -0
  104. data/lib/farce/signal.rb +1 -1
  105. data/lib/farce/strict/publisher.rb +31 -0
  106. data/lib/farce/strict/vector.rb +3 -2
  107. data/lib/farce/strict/weak_cache.rb +199 -0
  108. data/lib/farce/subscription/definitions.rb +27 -0
  109. data/lib/farce/subscription.rb +121 -0
  110. data/lib/farce/transaction/map.rb +35 -1
  111. data/lib/farce/unshared/pool.rb +1 -1
  112. data/lib/farce/unshared/publisher.rb +25 -0
  113. data/lib/farce/unshared/scheduler.rb +3 -3
  114. data/lib/farce/unshared/vector.rb +3 -2
  115. data/lib/farce/vector.rb +20 -16
  116. data/lib/farce/version.rb +1 -1
  117. data/lib/farce/walker/definitions.rb +31 -2
  118. data/lib/farce/walker/modification.rb +28 -14
  119. data/lib/farce/walker.rb +9 -1
  120. data/lib/farce/weak_atom.rb +5 -2
  121. data/lib/farce/weak_key_map.rb +1 -1
  122. data/lib/farce/weak_map.rb +1 -1
  123. data/lib/farce/weak_value_map.rb +1 -1
  124. data/lib/farce.rb +3 -1
  125. metadata +22 -2
@@ -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
@@ -61,7 +61,7 @@ module Farce
61
61
  STATES[self] = state
62
62
  GROUPS[group] = self
63
63
 
64
- state.main_thread = Thread.new do # rubocop:disable Lint/UselessSetterCall
64
+ state.main_thread = Thread.new do
65
65
  current = Thread.current
66
66
  group.add(current)
67
67
  current.freeze
@@ -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
@@ -8,11 +8,7 @@ module Farce
8
8
  module Freeze
9
9
  # Structurally freeze a Ruby facade, then validate and publish its object graph.
10
10
  # This deliberately bypasses an override that controls logical frozen state.
11
- def self.publish(object)
12
- Object.instance_method(:freeze).bind_call(object)
13
- ::Ractor.make_shareable(object) if Internal.native_ractors?
14
- object
15
- end
11
+ def self.publish(object) = Internal.publish_shareable(object)
16
12
 
17
13
  def self.check(object)
18
14
  return object unless object.frozen?
@@ -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
@@ -22,9 +22,7 @@ module Farce
22
22
  compare_keys_by_identity: compare_by_identity, compare_values_by_identity: compare_by_identity)
23
23
  initial_mapping = convert_entries(initial_mapping)
24
24
 
25
- identity = BasicObject.instance_method(:equal?)
26
- valid_comparison = identity.bind_call(compare_values_by_identity, true) ||
27
- identity.bind_call(compare_values_by_identity, false)
25
+ valid_comparison = true.equal?(compare_values_by_identity) || false.equal?(compare_values_by_identity)
28
26
 
29
27
  raise ArgumentError, "compare_values_by_identity must be a boolean" unless valid_comparison
30
28
  @compare_values_by_identity = compare_values_by_identity
@@ -33,9 +31,11 @@ module Farce
33
31
  @map = new_map(compare_by_identity:, compare_keys_by_identity:, compare_values_by_identity: true)
34
32
  KeyNormalizer.prepare_concurrent(@map)
35
33
 
36
- restoring = KeyNormalizer.restoration?(normalize_keys)
37
- normalizer = KeyNormalizer.build(normalize_keys, shareable: true)
38
- KeyNormalizer.install_concurrent(self, normalizer) unless restoring
34
+ unless nil.equal?(normalize_keys)
35
+ restoring = KeyNormalizer.restoration?(normalize_keys)
36
+ normalizer = KeyNormalizer.build(normalize_keys, shareable: true)
37
+ KeyNormalizer.install_concurrent(self, normalizer) unless restoring
38
+ end
39
39
 
40
40
  initial_mapping&.each { |key, value| self[key] = value }
41
41
  KeyNormalizer.install_concurrent(self, normalizer) if restoring
@@ -143,18 +143,26 @@ 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
- loop do
151
- current = @map.get_prepared(prepared_key, timeout: Internal.remaining_timeout(deadline)) { TIMED_OUT }
157
+ # JRuby rejects repeated anonymous block forwarding inside a block.
158
+ while true
159
+ current = @map.get_prepared(prepared_key, timeout: Clock.remaining(deadline)) { TIMED_OUT }
152
160
  return unwrap_result(current, &) if TIMED_OUT.equal?(current)
153
161
  return unwrap_value(current) unless values_equal?(nil.equal?(current) ? NIL_VALUE : current, expected)
154
162
  result = @map.wait_until_changed_prepared(
155
163
  prepared_key,
156
164
  current,
157
- timeout: Internal.remaining_timeout(deadline),
165
+ timeout: Clock.remaining(deadline),
158
166
  ) { TIMED_OUT }
159
167
  return unwrap_result(result, &) if TIMED_OUT.equal?(result)
160
168
  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
@@ -113,9 +113,20 @@ module Farce
113
113
  def storage_thread(thread) = thread
114
114
  def prepare_mutable_numeric(_) = nil
115
115
  def prepare_map_access(_klass, _kind) = nil
116
+ def prepare_shareable_class(_base) = nil
116
117
  def finalize_engine = nil
117
118
  def with_scheduler_thread_group = yield(ThreadGroup.new)
118
119
 
120
+ def initialize_range(target, first, last, exclude_end)
121
+ target.__send__(:initialize, first, last, exclude_end)
122
+ end
123
+
124
+ def publish_shareable(object)
125
+ Object.instance_method(:freeze).bind_call(object)
126
+ ::Ractor.make_shareable(object) if native_ractors?
127
+ object
128
+ end
129
+
119
130
  def delegate(from, to, *methods)
120
131
  methods.each do |method|
121
132
  if to.is_a?(Module)
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)
@@ -50,19 +50,13 @@ module Farce
50
50
 
51
51
  # Increment the counter atomically.
52
52
  # @param by [Numeric, String, #to_int] the amount, converted to an Integer
53
- # @return [self]
54
- def increment(by = 1)
55
- internal_counter.increment(by)
56
- self
57
- end
53
+ # @return [Integer] the value produced by this atomic update
54
+ def increment(by = 1) = internal_counter.increment(by)
58
55
 
59
56
  # Decrement the counter atomically.
60
57
  # @param by [Numeric, String, #to_int] the amount, converted to an Integer
61
- # @return [self]
62
- def decrement(by = 1)
63
- internal_counter.decrement(by)
64
- self
65
- end
58
+ # @return [Integer] the value produced by this atomic update
59
+ def decrement(by = 1) = internal_counter.decrement(by)
66
60
 
67
61
  alias subtract decrement
68
62
  alias remove decrement
@@ -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
@@ -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(...) = enqueue_task(true, ...)
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
@@ -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] the initial values
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(...) = Internal::UnsharedVector.new(...)
28
+ private def new_scoped_value(source = nil, **) = Internal::UnsharedVector.new(normalize_vector_source(source), **)
28
29
  end
29
30
  end
30
31
  end