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
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ class Subscription
7
+ Farce.on_main do
8
+ ractors = Internal.native_ractors? ? [::Ractor] : [Internal::FakeRactor::Main, Internal::FakeRactor::NotMain]
9
+
10
+ define(Internal::BasePort, *ractors) { subscriber.send(it) }
11
+ define(Abstract::Publisher) { subscriber.publish(it) }
12
+ define(Abstract::Queue) { subscriber.try_push(it) }
13
+ define(Internal::Counter, Local::Counter) { |_| subscriber.increment }
14
+ define(Internal::Flag, Local::Flag) { |_| subscriber.set }
15
+ define(Abstract::Atom) { subscriber.value = it }
16
+ define(Abstract::Set, Abstract::Vector) { subscriber << it }
17
+ define(Signal) { |_| subscriber.broadcast }
18
+
19
+ define(::Thread::Queue) { subscriber.push(it) }
20
+ define(::Thread::SizedQueue) do |payload|
21
+ subscriber.push(payload, true)
22
+ rescue ThreadError
23
+ false
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ # A publisher subscription that can be paused or permanently canceled.
7
+ # Disabling skips new messages. Unsubscribing drains an existing callback queue before stopping it.
8
+ class Subscription
9
+ include Internal::Freeze::Unfreezable
10
+ include Internal::Noncopyable
11
+
12
+ # Raised when an operation is attempted on an unsubscribed subscription.
13
+ UnsubscribedError = Class.new(StandardError)
14
+
15
+ REGISTER = ClassMirror.new(self)
16
+ private_constant :REGISTER
17
+
18
+ class << self
19
+ protected :new
20
+ end
21
+
22
+ # Register delivery behavior for subscriber classes from the main Ractor.
23
+ # Subclasses inherit the nearest registered delivery method.
24
+ #
25
+ # @example
26
+ # Farce::Subscription.define(MyInbox) { |payload| subscriber.push(payload) }
27
+ #
28
+ # @param classes [Array<Class>] the subscriber classes to support
29
+ # @yieldparam payload [BasicObject] the published payload
30
+ # @yieldreceiver [Subscription] the subscription whose {#subscriber} receives the payload
31
+ # @note The block must be convertible to a shareable proc.
32
+ # @return [void]
33
+ def self.define(*classes, &)
34
+ raise LocalJumpError, "no block given" unless block_given?
35
+ definition = Internal.prepare_method_definition(&)
36
+ classes.each do |klass|
37
+ REGISTER.define(klass) do
38
+ define_method(:deliver, definition)
39
+ private :deliver
40
+ end
41
+ end
42
+ end
43
+
44
+ # @api private
45
+ def self.build(publisher, subscriber, ...)
46
+ handler = REGISTER[subscriber.class]
47
+ unless handler.private_method_defined?(:deliver)
48
+ raise ArgumentError, "Unsupported subscriber type: #{subscriber.class}"
49
+ end
50
+ handler.new(publisher, subscriber, ...)
51
+ end
52
+
53
+ # @return [BasicObject] the subscriber associated with this subscription
54
+ attr_reader :subscriber
55
+
56
+ # @return [Farce::Abstract::Publisher] the publisher associated with this subscription
57
+ attr_reader :publisher
58
+
59
+ # @api private
60
+ attr_reader :key
61
+
62
+ # @!visibility private
63
+ def initialize(publisher, subscriber, map, callback: false, source: nil)
64
+ # Keep Proc identities alive without making their captured state shareable.
65
+ @source = source && Envelope.new(source, mode: :local)
66
+ @callback = callback
67
+ @enabled = Flag.new(true)
68
+ @key = (source || subscriber).__id__
69
+ @map = map
70
+ @publisher = publisher
71
+ @subscribed = Flag.new(true)
72
+ @subscriber = subscriber
73
+ super()
74
+ __freeze__
75
+ end
76
+
77
+ # @api private
78
+ def publish(payload)
79
+ return unless enabled?
80
+ deliver(payload)
81
+ true
82
+ rescue ::ClosedQueueError, Ractor::ClosedError, FrozenError
83
+ unsubscribe
84
+ false
85
+ end
86
+
87
+ # @return [Boolean] whether the subscription is currently enabled
88
+ def enabled? = @subscribed.value && @enabled.value
89
+
90
+ # @return [Boolean] whether the subscription has not been permanently canceled
91
+ def subscribed? = @subscribed.value
92
+
93
+ # Enables the subscription, allowing it to receive published payloads.
94
+ # @raise [UnsubscribedError] if the subscription has been unsubscribed
95
+ # @return [void]
96
+ def enable
97
+ raise UnsubscribedError, "Cannot enable a subscription that has been unsubscribed" unless @subscribed.value
98
+ @enabled.value = true
99
+ end
100
+
101
+ # Disables the subscription, preventing it from receiving published payloads.
102
+ # @return [void]
103
+ def disable = @enabled.value = false
104
+
105
+ # Removes the subscription permanently, allowing it and the subscriber to be garbage collected.
106
+ # @return [BasicObject, nil] the subscriber, or nil if already unsubscribed
107
+ def unsubscribe
108
+ return unless @subscribed.compare_and_set(true, false)
109
+ @map.delete_pair(key, self)
110
+ subscriber.seal if @callback
111
+ subscriber
112
+ end
113
+ alias cancel unsubscribe
114
+
115
+ private
116
+
117
+ define_method(:__freeze__, Kernel.instance_method(:freeze))
118
+
119
+ require "farce/subscription/definitions"
120
+ end
121
+ end
@@ -16,8 +16,42 @@ module Farce
16
16
  class Map < Abstract::ConcurrentMap
17
17
  include MapOperations
18
18
 
19
+ # (see Abstract::ConcurrentMap#store_if_present)
20
+ # @param mode [Symbol, nil] The result's transfer mode, or nil to use the default.
21
+ def store_if_present(key, mode: nil)
22
+ require_block!(block_given?)
23
+ write do
24
+ canonical = prepare(key)
25
+ next unless @working.key?(canonical)
26
+ replacement = yield(unwrap_stored(@working[canonical]))
27
+ unwrap_stored(store_value(canonical, wrap(replacement, mode:)))
28
+ end
29
+ end
30
+
31
+ # (see Abstract::ConcurrentMap#replace_if_exists)
32
+ # @param mode [Symbol, nil] The replacement's transfer mode, or nil to use the default.
33
+ def replace_if_exists(key, replacement, mode: nil)
34
+ write do
35
+ canonical = prepare(key)
36
+ next unless @working.key?(canonical)
37
+ previous = @working[canonical]
38
+ store_value(canonical, wrap(replacement, mode:))
39
+ unwrap_stored(previous)
40
+ end
41
+ end
42
+
43
+ # (see Abstract::ConcurrentMap#delete_pair)
44
+ # A failed comparison invalidates the transaction attempt.
45
+ def delete_pair(key, expected)
46
+ removed = modify_entry(key) do |present, value|
47
+ present && values_equal?(value, expected) ? Internal::MAP_DELETE : Internal::MAP_KEEP
48
+ end
49
+ compared(removed)
50
+ end
51
+
19
52
  Wrapper.inherit(self, *MapOperations::READ_HELPERS, :delete_if, :reject!, :compact!, :keep_if,
20
- :select!, :filter!, :transform_values!, :merge!)
53
+ :select!, :filter!, :transform_values!, :merge!, :put, :get_and_set, :compute_if_absent,
54
+ :compute_if_present, :replace_pair, :compute, :merge_pair, :fetch_or_store, :put_if_absent)
21
55
 
22
56
  private def enlist(backend) = @transaction.enlist(backend, per_key: true)
23
57
  end
@@ -36,7 +36,7 @@ module Farce
36
36
  Internal::LocalScheduledTask.new(args, block)
37
37
  end
38
38
 
39
- def schedule_scaler(started, delay = @grow_after)
39
+ def schedule_scaler(started, delay = settings.grow_after)
40
40
  Internal::ThreadPoolSupervisor.current.schedule(self, started, delay)
41
41
  end
42
42
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Unshared
7
+ # Unshared variant of {Farce::Publisher}.
8
+ class Publisher < Abstract::Publisher
9
+ include Unshareable
10
+
11
+ # @overload initialize(*subscribers)
12
+ # (see Farce::Abstract#initialize)
13
+ def initialize(...)
14
+ @map = Unshared::Map.new
15
+ super
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :map
21
+
22
+ def default_mode = :local
23
+ end
24
+ end
25
+ end
@@ -20,12 +20,12 @@ module Farce
20
20
  # results.pop # => :done
21
21
  # scheduler.close
22
22
  #
23
- # @!method schedule(*args, mode: :smart, auto_local: true)
23
+ # @!method schedule(*args, mode: :smart, auto_local: true, timeout: nil)
24
24
  # Enqueue a task while retaining its block and argument references.
25
25
  # `mode` is ignored when `auto_local` is true. Otherwise, only local and
26
26
  # smart modes are accepted.
27
27
  # @param (see Farce::Abstract::Executor#schedule)
28
- # @return [self]
28
+ # @return [self, nil] self when submitted, or nil when the scheduling timeout expires.
29
29
  class Scheduler < Abstract::Scheduler
30
30
  include Unshareable
31
31
  include Internal::LocalExecutorMethods
@@ -80,7 +80,7 @@ module Farce
80
80
  def new_queue(capacity:) = Internal::UnsharedQueue.new(capacity:)
81
81
  def launch_callback = ->(_scheduler, priority, name) { launch!(name, priority) }
82
82
 
83
- def prepare_task(args, block, mode, auto_local)
83
+ def prepare_task(args, block, mode, auto_local, **)
84
84
  validate_local_mode(mode, auto_local)
85
85
  Internal::LocalScheduledTask.new(args, block)
86
86
  end
@@ -9,10 +9,11 @@ module Farce
9
9
  include Unshareable
10
10
  include Internal::Freeze::Delegated
11
11
 
12
- # @param source [Array, nil] Initial values. Values are retained directly.
12
+ # @param source [Array, Integer, nil] Initial values or a size filled with nil. Values are retained directly.
13
13
  # @param compare_by_identity [Boolean] Whether values are compared by identity.
14
+ # @raise [ArgumentError] If the size is negative.
14
15
  def initialize(source = nil, compare_by_identity: false)
15
- @vector = Internal::UnsharedVector.new(source, compare_by_identity:)
16
+ @vector = Internal::UnsharedVector.new(normalize_vector_source(source), compare_by_identity:)
16
17
  super()
17
18
  end
18
19
 
data/lib/farce/vector.rb CHANGED
@@ -20,18 +20,21 @@ module Farce
20
20
  include Shareable::Delegated
21
21
 
22
22
  # @!macro modes
23
- # @param source [Array, nil] Initial values. The source array is not retained.
23
+ # @param source [Array, Integer, nil] Initial values or a size filled with nil. The source array is not retained.
24
24
  # @param mode [Symbol] The default value transfer mode.
25
25
  # @param compare_by_identity [Boolean] Whether values are compared by identity.
26
+ # @raise [ArgumentError] If the size is negative.
26
27
  def initialize(source = nil, mode: :smart, compare_by_identity: false)
27
- raise TypeError, "source must be an Array" unless source.nil? || source.is_a?(Array)
28
- unless true.equal?(compare_by_identity) || false.equal?(compare_by_identity)
29
- raise ArgumentError, "compare_by_identity must be true or false"
30
- end
28
+ source = normalize_vector_source(source)
29
+ raise TypeError, "source must be an Array" unless source.nil? || Integer === source || source.is_a?(Array)
31
30
 
32
- @compare_by_identity = compare_by_identity
31
+ @compare_by_identity = compare_by_identity ? true : false
33
32
  @manager = ModeManager.new(mode:)
34
- @vector = Internal::Vector.new(source&.map { @manager.wrap(it) }, compare_by_identity: true)
33
+ if source && !(Integer === source)
34
+ source = source.map { @manager.wrap(it) }
35
+ raise TypeError, "source must be an Array" unless source.nil? || source.is_a?(Array)
36
+ end
37
+ @vector = Internal::Vector.new_for_modes(source)
35
38
  super()
36
39
  end
37
40
 
@@ -51,7 +54,7 @@ module Farce
51
54
  def marshal_load(data)
52
55
  entries, @manager, identity, frozen = Internal::MarshalSupport.payload(data, 4)
53
56
  @compare_by_identity = identity
54
- @vector = Internal::Vector.new(compare_by_identity: true)
57
+ @vector = Internal::Vector.new_for_modes(nil)
55
58
  publish_shareable
56
59
  entries.each { @vector.push(Internal::MarshalSupport.restore_value(it)) }
57
60
  Internal::MarshalSupport.freeze(self, frozen)
@@ -176,11 +179,18 @@ module Farce
176
179
  replacement = @manager.wrap(replacement, mode:)
177
180
  wrapped = true
178
181
  end
179
- return true if @vector.compare_and_set(index, current, replacement, timeout: remaining_timeout(deadline))
182
+ return true if @vector.compare_and_set(index, current, replacement, timeout: Clock.remaining(deadline))
180
183
  return false if nil.equal?(current) || (deadline && Clock.now >= deadline)
181
184
  end
182
185
  end
183
186
 
187
+ # (see Farce::Abstract::Vector#wait_until)
188
+ def wait_until(index, timeout: nil)
189
+ raise LocalJumpError, "no block given" unless block_given?
190
+ result = Internal.wait_until(@vector, index, timeout:) { |current| yield(@manager.unwrap(current)) }
191
+ @manager.unwrap(result)
192
+ end
193
+
184
194
  # (see Farce::Abstract::Vector#wait_until_changed)
185
195
  def wait_until_changed(index, expected, timeout: nil)
186
196
  deadline = timeout_deadline(timeout)
@@ -189,7 +199,7 @@ module Farce
189
199
  current = @vector[index]
190
200
  return @manager.unwrap(current) unless values_equal?(current, expected)
191
201
  return if deadline && Clock.now >= deadline
192
- @vector.wait_until_changed(index, current, timeout: remaining_timeout(deadline))
202
+ @vector.wait_until_changed(index, current, timeout: Clock.remaining(deadline))
193
203
  end
194
204
  end
195
205
 
@@ -243,11 +253,5 @@ module Farce
243
253
  raise ArgumentError, "timeout must be finite and non-negative" unless timeout.finite? && !timeout.negative?
244
254
  Clock.now + timeout
245
255
  end
246
-
247
- def remaining_timeout(deadline)
248
- return unless deadline
249
- remaining = deadline - Clock.now
250
- remaining.negative? ? 0 : remaining
251
- end
252
256
  end
253
257
  end
data/lib/farce/version.rb CHANGED
@@ -4,5 +4,5 @@
4
4
 
5
5
  module Farce
6
6
  # @return [String] the current version of Farce
7
- VERSION = "0.4.1"
7
+ VERSION = "0.6.0"
8
8
  end
@@ -118,8 +118,19 @@ module Farce
118
118
  if !key.equal?(new_key)
119
119
  entries << [key, new_key, result]
120
120
  elsif !value.equal?(result)
121
- until target.compare_and_set(key, value, result)
122
- value = target[key]
121
+ loop do
122
+ retry_update = false
123
+ send_to(target, :modify_entry, key, canonical: true) do |present, current|
124
+ next Internal::MAP_KEEP unless present
125
+ if Internal.value_changed?(current, value, true)
126
+ value = current
127
+ retry_update = true
128
+ next Internal::MAP_KEEP
129
+ end
130
+ result
131
+ end
132
+ break unless retry_update
133
+ # Traverse replacements after releasing the key's update reservation.
123
134
  result = walker.visit(value)
124
135
  end
125
136
  end
@@ -179,6 +190,24 @@ module Farce
179
190
  super(object, walker)
180
191
  end
181
192
 
193
+ define(Struct) do |object, walker|
194
+ result = walker.update(object, object.to_a) do |target, results|
195
+ results.each_with_index { |value, index| target[index] = value unless target[index].equal?(value) }
196
+ target
197
+ end
198
+ super(result, walker)
199
+ end
200
+
201
+ define(Range) do |object, walker|
202
+ names = object.instance_variables
203
+ values = [object.begin, object.end, *names.map { object.instance_variable_get(it) }]
204
+ walker.update(object, values) do |target, results|
205
+ names.each_with_index { |name, index| target.instance_variable_set(name, results[index + 2]) }
206
+ Internal.initialize_range(target, results[0], results[1], object.exclude_end?)
207
+ target
208
+ end
209
+ end
210
+
182
211
  define(Data) do |object, walker|
183
212
  walker.rebuild_data(object)
184
213
  end
@@ -172,14 +172,14 @@ module Farce
172
172
  node ? node.changed == true : (@seen.key?(object) && !@seen[object].equal?(object))
173
173
  end
174
174
 
175
- # Follow pending references to check that hashing cannot reach unfinished Data.
175
+ # Follow pending references to check that hashing cannot reach unfinished values.
176
176
  def hashable?(object)
177
- return true unless @unfinished_data && !@unfinished_data.empty?
177
+ return true unless @unfinished_values && !@unfinished_values.empty?
178
178
  !Walker.visit(object, constants:, class_variables:) do |value, traversal|
179
- traversal.return(true) if @unfinished_data.key?(value)
179
+ traversal.return(true) if @unfinished_values.key?(value)
180
180
  node = @nodes[value] || @destinations&.[](value)
181
181
  if node && node.plans
182
- traversal.return(true) if @unfinished_data.key?(node.destination)
182
+ traversal.return(true) if @unfinished_values.key?(node.destination)
183
183
  node.plans.each { |plan| plan.children.each { traversal.visit(resolved(it)) } }
184
184
  else
185
185
  traversal.traverse
@@ -188,10 +188,10 @@ module Farce
188
188
  end
189
189
  end
190
190
 
191
- # Reject keys and set elements whose Data dependencies are still being built.
191
+ # Reject keys and set elements whose immutable dependencies are still being built.
192
192
  def check_hash_key(object)
193
193
  return object if hashable?(object)
194
- raise ArgumentError, "hash key or set element refers to an unfinished Data value"
194
+ raise ArgumentError, "hash key or set element refers to an unfinished Data or Range value"
195
195
  end
196
196
 
197
197
  private
@@ -217,19 +217,19 @@ module Farce
217
217
  dirty ||= plan.hash_keys && (index % plan.key_stride).zero? && child && child.changed
218
218
  end
219
219
  node.changed ||= dirty
220
- plan.dirty = dirty
220
+ plan.dirty = dirty || (Range === node.destination && @unfinished_values&.key?(node.destination))
221
221
  end
222
222
 
223
- # Choose a writable destination, copying lazily or allocating unfinished Data.
223
+ # Choose a writable destination, copying lazily or allocating unfinished values.
224
224
  def prepare(node, rebuild: false)
225
225
  previous = node.destination
226
226
  return previous unless rebuild || previous.equal?(node.original)
227
227
  object = node.original
228
228
  return object if Internal::Noncopyable === object || Abstract::LeaseMap === object
229
229
  destination =
230
- if Data === object
230
+ if Data === object || Range === object
231
231
  value = object.class.allocate
232
- (@unfinished_data ||= {}.compare_by_identity)[value] = true
232
+ (@unfinished_values ||= {}.compare_by_identity)[value] = true
233
233
  value
234
234
  elsif @modify == true
235
235
  send_to(object, :frozen?) ? send_to(object, :clone, freeze: false) : object
@@ -241,6 +241,7 @@ module Farce
241
241
  (@destinations ||= {}.compare_by_identity)[destination] = node unless destination.equal?(object)
242
242
  node.destination = destination
243
243
  node.result = destination if !node.skipped && (node.result.equal?(object) || node.result.equal?(previous))
244
+ node.plans&.each { it.dirty = true } if Range === destination
244
245
  destination
245
246
  end
246
247
 
@@ -259,7 +260,7 @@ module Farce
259
260
  node.destination = result
260
261
  node.result = result if node.result.equal?(old)
261
262
  end
262
- @unfinished_data&.delete(node.destination) if Data === node.destination
263
+ @unfinished_values&.delete(node.destination) if Data === node.destination || Range === node.destination
263
264
  return unless node.cyclic
264
265
  plan.target = node.destination
265
266
  plan.applied = plan.results.dup
@@ -281,6 +282,9 @@ module Farce
281
282
  if freeze
282
283
  node.changed = true unless send_to(node.result, :frozen?)
283
284
  prepare(node) if @modify != true && node.result.equal?(node.original) && !send_to(node.result, :frozen?)
285
+ if Range === node.destination && @unfinished_values&.key?(node.destination)
286
+ node.plans&.each { apply(node, it) }
287
+ end
284
288
  send_to(node.result, :freeze)
285
289
  end
286
290
  finalize_node(node) if finalize
@@ -308,15 +312,25 @@ module Farce
308
312
  freezing = !node.skipped && (node.freeze_result || @freeze == true)
309
313
  needed = node.plans&.any?(&:dirty) || (freezing && @modify != true && !send_to(node.original, :frozen?))
310
314
  prepare(node) if needed
311
- next unless Data === node.destination && send_to(node.destination, :frozen?) && node.plans&.any? do |plan|
312
- plan.applied && plan.results.each_with_index.any? { |value, index| !value.equal?(plan.applied[index]) }
315
+ next unless Data === node.destination || Range === node.destination
316
+ # Rebuild only after this destination's own assignments become stale.
317
+ next unless node.plans&.any? do |plan|
318
+ plan.target.equal?(node.destination) && plan.applied &&
319
+ plan.results.each_with_index.any? { |value, index| !value.equal?(plan.applied[index]) }
313
320
  end
314
321
  prepare(node, rebuild: true)
315
322
  end
316
323
  break if component.each_with_index.all? { |node, index| node.destination.equal?(destinations[index]) }
317
324
  end
318
325
 
319
- component.sort_by { Data === it.destination ? 2 : (it.plans&.any?(&:hash_keys) ? 1 : 0) }.each do |node|
326
+ ordered = component.sort_by do |node|
327
+ if Data === node.destination || Range === node.destination
328
+ 2
329
+ else
330
+ node.plans&.any?(&:hash_keys) ? 1 : 0
331
+ end
332
+ end
333
+ ordered.each do |node|
320
334
  @node = node
321
335
  @current_object = node.original
322
336
  node.plans&.each do |plan|
data/lib/farce/walker.rb CHANGED
@@ -140,14 +140,18 @@ module Farce
140
140
  end
141
141
 
142
142
  # Transform a graph using the callback's return values.
143
+ #
143
144
  # Call {#traverse} to transform an object's children. Returning another value
144
145
  # replaces the object without automatically visiting that replacement.
145
146
  # Unchanged branches retain their identity, including when copy is enabled.
147
+ #
146
148
  # Changed cycles are connected before results are frozen or finalized.
147
149
  # A cyclic callback may run again as child replacements become known. Keep
148
150
  # callbacks repeatable and put publication or caching in {#finalize}.
149
151
  # Use {#freeze_result} instead of freezing a preliminary traversal result.
152
+ #
150
153
  # Data values are rebuilt only when members change, using their normal initializer.
154
+ # Ranges are rebuilt when endpoints or instance variables change.
151
155
  # Noncopyable coordination objects, such as leases, are updated in place.
152
156
  # Constants and class variables are assigned only when their value
153
157
  # changes identity. Constant replacement can emit Ruby redefinition warnings.
@@ -156,6 +160,7 @@ module Farce
156
160
  #
157
161
  # A walk is not an atomic snapshot or update. Coordinate concurrent writers
158
162
  # when transforming container structure, especially map keys.
163
+ #
159
164
  # @param object [BasicObject] the root object
160
165
  # @!macro walker_module_options
161
166
  # @param copy [Boolean, Symbol] false to edit mutable objects in place, true to
@@ -171,7 +176,7 @@ module Farce
171
176
  # @raise [ArgumentError] if copy is neither a boolean nor a Symbol
172
177
  # @raise [ArgumentError] if cyclic callbacks do not converge within 32 passes,
173
178
  # or a cyclic finalizer replaces its result
174
- # @raise [ArgumentError] if a hash key or set element refers to a Data value
179
+ # @raise [ArgumentError] if a hash key or set element refers to a Data or Range value
175
180
  # still being constructed. Identity-based containers do not hash their keys.
176
181
  def self.modify(object, copy: false, freeze: nil, constants: true, class_variables: true, &callback)
177
182
  raise LocalJumpError, "no block given" unless callback
@@ -263,9 +268,11 @@ module Farce
263
268
  def remember(object, result) = @seen[object] = result
264
269
 
265
270
  # Visit child values and describe how to assign their replacements.
271
+ #
266
272
  # The assignment block only runs during modification and may run again for cycles.
267
273
  # It receives a writable target and the resolved children. Do not mutate object
268
274
  # outside this block. Return the result of update from a traversal definition.
275
+ #
269
276
  # @param object [BasicObject] the object being traversed
270
277
  # @param values [Array] child references in assignment order
271
278
  # @param hash_keys [Boolean] rebuild when key descendants change in place
@@ -288,6 +295,7 @@ module Farce
288
295
  # and requested freezing. Registering another finalizer replaces the first.
289
296
  # Cyclic finalizers must preserve identity. Acyclic finalizers may return a
290
297
  # canonical replacement, which is propagated to the parent.
298
+ #
291
299
  # @yieldparam result [BasicObject] the settled result
292
300
  # @yieldparam cyclic [Boolean] whether the node belongs to a cycle
293
301
  # @yieldreturn [BasicObject] the final result
@@ -100,14 +100,17 @@ module Farce
100
100
  matched
101
101
  end
102
102
 
103
+ # (see Abstract::Atom#wait_until)
104
+ def wait_until(timeout: nil, &) = Internal.wait_until(@atom, timeout:, &)
105
+
103
106
  # (see Abstract::Atom#wait_until_changed)
104
107
  def wait_until_changed(expected, timeout: nil, &fallback)
105
108
  deadline = Internal.timeout_deadline(timeout)
106
109
  while true
107
- current = @atom.get(timeout: Internal.remaining_timeout(deadline)) { TIMED_OUT }
110
+ current = @atom.get(timeout: Clock.remaining(deadline)) { TIMED_OUT }
108
111
  return fallback&.call if TIMED_OUT.equal?(current)
109
112
  return current unless values_equal?(current, expected)
110
- result = @atom.wait_until_changed(current, timeout: Internal.remaining_timeout(deadline)) { TIMED_OUT }
113
+ result = @atom.wait_until_changed(current, timeout: Clock.remaining(deadline)) { TIMED_OUT }
111
114
  return fallback&.call if TIMED_OUT.equal?(result)
112
115
  end
113
116
  end
@@ -23,7 +23,7 @@ module Farce
23
23
 
24
24
  private
25
25
 
26
- def new_map(**) = Internal::StrictWeakKeyMap.new(**)
26
+ def new_map(...) = Internal::StrictWeakKeyMap.new(...)
27
27
  def freeze_backend = @map
28
28
  end
29
29
 
@@ -25,7 +25,7 @@ module Farce
25
25
 
26
26
  private
27
27
 
28
- def new_map(**) = Internal::StrictWeakMap.new(**)
28
+ def new_map(...) = Internal::StrictWeakMap.new(...)
29
29
  def freeze_backend = @map
30
30
  end
31
31
 
@@ -24,7 +24,7 @@ module Farce
24
24
 
25
25
  private
26
26
 
27
- def new_map(**) = Internal::StrictWeakValueMap.new(**)
27
+ def new_map(...) = Internal::StrictWeakValueMap.new(...)
28
28
  def freeze_backend = @map
29
29
  end
30
30
 
data/lib/farce.rb CHANGED
@@ -429,7 +429,9 @@ module Farce
429
429
  end
430
430
 
431
431
  # Schedules work without waiting for the block to finish.
432
- # On CRuby, uses a shared Ractor pool with at most {System.cpu_count} workers.
432
+ # On CRuby, uses a shared Ractor pool with at most {System.cpu_count} workers by default.
433
+ # Set {Config}'s `parallel_pool_*` options before creating a scheduler to configure
434
+ # its worker limits, growth and shrink delays, per-worker task limit, and queue capacity.
433
435
  # On JRuby and TruffleRuby, starts a new thread for each task.
434
436
  #
435
437
  # Without a block, returns the shared scheduler. The CRuby pool starts workers