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,136 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Internal
7
+ class PortableTrie
8
+ # @api private
9
+ module IgnoreCase
10
+ private
11
+
12
+ def each_result(input, peek:, deduplicate:, &)
13
+ raise TypeError, "input must be a String" unless input.is_a?(String)
14
+ input = String.new(input)
15
+ validate_encoding!(input, "input")
16
+
17
+ input_length = input.length
18
+ static_input = input.downcase
19
+ if static_input.length != input_length
20
+ return each_expanding_result(input, static_input, peek:, deduplicate:, &)
21
+ end
22
+ seen = {} if deduplicate
23
+ stack = [[@root, 0, EMPTY_CAPTURES, EMPTY_NAMED_CAPTURES, 0, 0]]
24
+
25
+ until stack.empty?
26
+ node, position, captures, named, phase, index = stack.pop
27
+ if phase.zero?
28
+ child = nil
29
+ stride = node.stride
30
+ if node.fast_static
31
+ key = static_input[position, stride]
32
+ child = node.fast_static[key] if key
33
+ elsif position < input_length && !node.static.empty?
34
+ child = node.static[static_input[position]]
35
+ end
36
+ if child
37
+ # Retain only frames with alternatives or a terminal result.
38
+ stack << [node, position, captures, named, 1, 0] unless node.dynamic.empty? && node.tokens.empty?
39
+ stack << [child, position + stride, captures, named, 0, 0]
40
+ next
41
+ end
42
+ end
43
+ if index < node.dynamic.length
44
+ regexp, child = node.dynamic[index]
45
+ if index + 1 < node.dynamic.length || !node.tokens.empty?
46
+ stack << [node, position, captures, named, 1, index + 1]
47
+ end
48
+ if (matched = regexp.match(input, position))
49
+ stack << [
50
+ child,
51
+ matched.end(0),
52
+ captures + matched.captures,
53
+ merge_named_captures(named, matched.named_captures),
54
+ 0,
55
+ 0
56
+ ]
57
+ end
58
+ elsif (peek || position == input_length) && !node.tokens.empty?
59
+ node.tokens.each do |token_id|
60
+ next if deduplicate && seen.key?(token_id)
61
+
62
+ seen[token_id] = true if deduplicate
63
+ yield(
64
+ @tokens.fetch(token_id),
65
+ captures.map { |value| value&.dup },
66
+ copy_named_captures(named),
67
+ input[position..] || +""
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def each_expanding_result(input, static_input, peek:, deduplicate:)
75
+ to_original = [0]
76
+ from_original = [0]
77
+ input.each_char.with_index do |character, index|
78
+ count = character.downcase.length
79
+ # An internal lowercase boundary cannot start a regexp or end a result.
80
+ (count - 1).times { to_original << nil }
81
+ to_original << (index + 1)
82
+ from_original << (to_original.length - 1)
83
+ end
84
+ input_length = static_input.length
85
+ seen = {} if deduplicate
86
+ stack = [[@root, 0, EMPTY_CAPTURES, EMPTY_NAMED_CAPTURES, 0, 0]]
87
+
88
+ until stack.empty?
89
+ node, position, captures, named, phase, index = stack.pop
90
+ original_position = to_original[position]
91
+ case phase
92
+ when 0
93
+ stack << [node, position, captures, named, 1, 0]
94
+ if node.fast_static
95
+ key = static_input[position, node.stride]
96
+ if key && (child = node.fast_static[key])
97
+ stack << [child, position + node.stride, captures, named, 0, 0]
98
+ end
99
+ elsif position < input_length && (child = node.static[static_input[position]])
100
+ stack << [child, position + 1, captures, named, 0, 0]
101
+ end
102
+ when 1
103
+ if index < node.dynamic.length
104
+ regexp, child = node.dynamic[index]
105
+ stack << [node, position, captures, named, 1, index + 1]
106
+ if original_position && (matched = regexp.match(input, original_position))
107
+ stack << [
108
+ child,
109
+ from_original[matched.end(0)],
110
+ captures + matched.captures,
111
+ merge_named_captures(named, matched.named_captures),
112
+ 0,
113
+ 0
114
+ ]
115
+ end
116
+ elsif original_position && (peek || position == input_length) && !node.tokens.empty?
117
+ node.tokens.each do |token_id|
118
+ next if deduplicate && seen.key?(token_id)
119
+
120
+ seen[token_id] = true if deduplicate
121
+ yield(
122
+ @tokens.fetch(token_id),
123
+ captures.map { |value| value&.dup },
124
+ copy_named_captures(named),
125
+ input[original_position..] || +""
126
+ )
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ private_constant :IgnoreCase
134
+ end
135
+ end
136
+ end
@@ -76,11 +76,16 @@ module Farce
76
76
  end
77
77
  private_constant :Node
78
78
 
79
- def initialize(entries, tokens)
79
+ def initialize(entries, tokens, ignore_case: false)
80
80
  validate_array!(entries, "entries")
81
81
  validate_array!(tokens, "tokens")
82
82
  validate_tokens!(tokens)
83
83
 
84
+ if ignore_case
85
+ @ignore_case = true
86
+ extend IgnoreCase
87
+ end
88
+
84
89
  @tokens = Array.new(tokens).freeze
85
90
  @root = Node.new
86
91
  nodes = [@root]
@@ -103,6 +108,7 @@ module Farce
103
108
  when String
104
109
  literal = String.new(part)
105
110
  validate_encoding!(literal, "edge")
111
+ literal.downcase! if ignore_case
106
112
  literal.each_char do |character|
107
113
  child = node.static[character]
108
114
  unless child
@@ -171,6 +177,12 @@ module Farce
171
177
 
172
178
  private
173
179
 
180
+ def initialize_dup(other)
181
+ super
182
+ # Unlike clone, dup does not retain the instance's matching module.
183
+ extend IgnoreCase if @ignore_case
184
+ end
185
+
174
186
  def anchor(regexp)
175
187
  compile_regexp("\\G(?:#{regexp.source})", regexp)
176
188
  rescue RegexpError
@@ -314,4 +326,5 @@ module Farce
314
326
  end
315
327
  end
316
328
 
329
+ require "farce/engine/shared/trie/ignore_case"
317
330
  require "farce/engine/shared/trie_builder"
@@ -17,6 +17,13 @@ module Farce
17
17
  # terminal match. A regexp edge is atomic. Later edges cannot make a regexp
18
18
  # choose a different endpoint.
19
19
  #
20
+ # Pass `ignore_case: true` to {.build} or {Builder.new} to compare all static
21
+ # segments using Unicode downcasing. Regexp options do not change. Regexp
22
+ # matches, captures, and suffixes use the original input and retain its casing.
23
+ # This does not include every equivalence supported by `Regexp::IGNORECASE`.
24
+ # For example, `"SS"` matches `"ss"`, but `"ß"` does not match `"ss"`.
25
+ # Case-equivalent static entries share a branch and retain their token order.
26
+ #
20
27
  # Tokens must already be Ractor-shareable. They are stored and returned
21
28
  # unchanged. Equal but different token objects remain separate. Reusing the
22
29
  # same token object produces at most one result per lookup.
@@ -94,20 +101,23 @@ module Farce
94
101
  # Collects entries and publishes immutable {Trie} snapshots.
95
102
  class Builder
96
103
  # Create an empty builder.
97
- def initialize
104
+ # @param ignore_case [Boolean] compare all static segments without case
105
+ def initialize(ignore_case: false)
98
106
  @entries = []
107
+ @ignore_case = !!ignore_case
99
108
  end
100
109
 
101
110
  # @api private
102
111
  def marshal_dump
103
112
  entries = @entries.map { |parts, token| [parts, Internal::MarshalSupport.value(token)] }
104
- [1, entries, frozen?]
113
+ [1, entries, frozen?, @ignore_case]
105
114
  end
106
115
 
107
116
  # @api private
108
117
  def marshal_load(data)
109
- entries, frozen = Internal::MarshalSupport.payload(data, 2)
110
- initialize
118
+ length = data.is_a?(Array) && data.length == 3 ? 2 : 3
119
+ entries, frozen, ignore_case = Internal::MarshalSupport.payload(data, length)
120
+ initialize(ignore_case:)
111
121
  entries.each { |parts, token| add(parts, Internal::MarshalSupport.restore_value(token)) }
112
122
  freeze if frozen
113
123
  end
@@ -159,7 +169,7 @@ module Farce
159
169
  [parts.dup.freeze, token_ids.fetch(token)].freeze
160
170
  end.freeze
161
171
 
162
- Trie.send(:build_entries, entries, tokens.freeze)
172
+ Trie.send(:build_entries, entries, tokens.freeze, ignore_case: @ignore_case)
163
173
  end
164
174
 
165
175
  private
@@ -180,17 +190,18 @@ module Farce
180
190
  class << self
181
191
  # Build an immutable trie.
182
192
  #
193
+ # @param ignore_case [Boolean] compare all static segments without case
183
194
  # @yieldparam builder [Builder]
184
195
  # @return [Trie]
185
- def build
186
- builder = Builder.new
196
+ def build(ignore_case: false)
197
+ builder = Builder.new(ignore_case:)
187
198
  yield builder if block_given?
188
199
  builder.build
189
200
  end
190
201
 
191
202
  private
192
203
 
193
- def build_entries(entries, tokens) = new(entries, tokens)
204
+ def build_entries(entries, tokens, ignore_case:) = new(entries, tokens, ignore_case:)
194
205
 
195
206
  private :new
196
207
  end
@@ -129,7 +129,7 @@ module Farce
129
129
 
130
130
  loop do
131
131
  current, version = @mutex.synchronize { [@value, @version] }
132
- return current unless values_equal?(current, expected)
132
+ return current if Internal.value_changed?(current, expected, compare_by_identity?)
133
133
 
134
134
  result = @mutex.synchronize do
135
135
  next unless @version == version
@@ -6,9 +6,11 @@ module Farce
6
6
  # @!visibility private
7
7
  module Internal # :nodoc: all
8
8
  class UnsharedVector
9
+ def self.normalize_source(source) = source.is_a?(Integer) ? Array.new(source) : source
10
+
9
11
  def initialize(source = nil, compare_by_identity: false)
10
12
  validate_boolean(compare_by_identity, "compare_by_identity")
11
- raise TypeError, "source must be an Array" unless source.nil? || source.is_a?(Array)
13
+ raise TypeError, "source must be an Array" unless source.nil? || source.is_a?(Array) || source.is_a?(Integer)
12
14
 
13
15
  @values = source ? Array.new(source) : []
14
16
  @compare_by_identity = compare_by_identity
@@ -274,12 +276,12 @@ module Farce
274
276
  finish_update if completed
275
277
  end
276
278
 
277
- def wait_until_changed(index, expected, timeout: nil)
278
- wait_for_value(convert_index(index), expected, timeout_deadline(timeout), non_nil: false)
279
+ def wait_until_changed(index, expected, timeout: nil, &fallback)
280
+ wait_for_value(convert_index(index), expected, timeout_deadline(timeout), fallback, non_nil: false)
279
281
  end
280
282
 
281
283
  def wait_until_non_nil(index, timeout: nil)
282
- wait_for_value(convert_index(index), nil, timeout_deadline(timeout), non_nil: true)
284
+ wait_for_value(convert_index(index), nil, timeout_deadline(timeout), nil, non_nil: true)
283
285
  end
284
286
 
285
287
  def compare_by_identity? = @compare_by_identity
@@ -341,20 +343,21 @@ module Farce
341
343
  raise FrozenError.new("can't modify frozen #{self.class}", receiver: self)
342
344
  end
343
345
 
344
- def wait_for_value(index, expected, deadline, non_nil:)
346
+ def wait_for_value(index, expected, deadline, fallback, non_nil:)
345
347
  while true
346
348
  generation = @signal.generation
347
349
  current = @mutex.synchronize { @values[index] }
348
- ready = non_nil ? !current.nil? : !values_equal?(current, expected)
350
+ ready = non_nil ? !current.nil? :
351
+ Internal.value_changed?(current, expected, compare_by_identity?)
349
352
  return current if ready
350
- return unless wait_for_signal(generation, deadline)
353
+ return fallback&.call unless wait_for_signal(generation, deadline)
351
354
  end
352
355
  end
353
356
 
354
357
  def wait_for_signal(generation, deadline)
355
358
  return @signal.wait(generation) unless deadline
356
359
 
357
- remaining = deadline - Clock.now
360
+ remaining = Clock.remaining(deadline)
358
361
  return false unless remaining.positive?
359
362
  !@signal.wait(generation, timeout: remaining).nil?
360
363
  end
@@ -145,10 +145,11 @@ module Farce
145
145
  # Yield presence and value under the entry reservation.
146
146
  # MAP_KEEP and MAP_DELETE are control results. Other results replace the value.
147
147
  # Return whether a change committed.
148
- def modify(key) # rubocop:disable Naming/PredicateMethod
148
+ def modify(key, timeout: nil)
149
149
  raise LocalJumpError, "no block given" unless block_given?
150
150
  check_frozen!
151
- _, result = with_entry(key, nil, create: true, mutating: true) do |present, current, entry, index|
151
+ deadline = timeout_deadline(timeout)
152
+ completed, result = with_entry(key, deadline, create: true, mutating: true) do |present, current, entry, index|
152
153
  value = yield(present, current)
153
154
  check_frozen!
154
155
  if MAP_KEEP.equal?(value)
@@ -161,7 +162,7 @@ module Farce
161
162
  entry.store(value)
162
163
  end
163
164
  end
164
- !!result
165
+ completed && !!result
165
166
  end
166
167
 
167
168
  def wait_until_changed(key, expected, timeout: nil, &fallback)
@@ -352,7 +353,7 @@ module Farce
352
353
  return fallback&.call if timed_out == :timed_out
353
354
 
354
355
  unless entry
355
- return nil unless values_equal?(nil, expected)
356
+ return nil if Internal.value_changed?(nil, expected, compare_values_by_identity?)
356
357
  return fallback&.call unless signal_changed?(@index.change_signal, observed, deadline)
357
358
  next
358
359
  end
@@ -373,13 +374,14 @@ module Farce
373
374
  # lookup object. Otherwise GC could orphan the entry's change signal.
374
375
  key = stored_key
375
376
  current = nil unless present
376
- return current if non_nil ? !current.nil? : !values_equal?(current, expected)
377
+ return current if non_nil ? !current.nil? :
378
+ Internal.value_changed?(current, expected, compare_values_by_identity?)
377
379
  return fallback&.call unless entry.wait_for_change?(changed, deadline)
378
380
  end
379
381
  end
380
382
 
381
383
  def signal_changed?(signal, observed, deadline)
382
- timeout = deadline - Clock.now if deadline
384
+ timeout = Clock.remaining(deadline)
383
385
  return false if timeout && !timeout.positive?
384
386
 
385
387
  timed_out = Object.new
@@ -6,6 +6,8 @@ module Farce
6
6
  # @!visibility private
7
7
  module Internal # :nodoc: all
8
8
  class Vector < UnsharedVector
9
+ def self.new_for_modes(source) = new(source, compare_by_identity: true)
10
+
9
11
  def initialize(source = nil, **)
10
12
  source.each { check_value(it) } if source.is_a?(Array)
11
13
  super
@@ -146,7 +146,7 @@ module Farce
146
146
  def with_value(timeout)
147
147
  deadline = timeout_deadline(timeout)
148
148
  result = TIMED_OUT
149
- @lock.update(timeout: remaining_timeout(deadline)) do
149
+ @lock.update(timeout: Clock.remaining(deadline)) do
150
150
  result = yield
151
151
  nil
152
152
  end
@@ -158,11 +158,12 @@ module Farce
158
158
  loop do
159
159
  generation = @changes.value
160
160
  current = read_value
161
- return current if non_nil ? !current.nil? : !values_equal?(current, expected)
161
+ return current if non_nil ? !current.nil? :
162
+ Internal.value_changed?(current, expected, compare_by_identity?)
162
163
  # Detect recursive waits without waiting for another owner's update.
163
164
  @lock.get(timeout: 0)
164
- return fallback&.call if remaining_timeout(deadline)&.zero?
165
- @changes.wait_until_changed(generation, timeout: remaining_timeout(deadline))
165
+ return fallback&.call if Clock.remaining(deadline)&.zero?
166
+ @changes.wait_until_changed(generation, timeout: Clock.remaining(deadline))
166
167
  end
167
168
  end
168
169
 
@@ -177,12 +178,6 @@ module Farce
177
178
  raise ArgumentError, "timeout must be finite and non-negative" unless timeout.finite? && !timeout.negative?
178
179
  Clock.now + timeout
179
180
  end
180
-
181
- def remaining_timeout(deadline)
182
- return unless deadline
183
- remaining = deadline - Clock.now
184
- remaining.negative? ? 0 : remaining
185
- end
186
181
  end
187
182
  private_constant :WeakAtomBase
188
183
  end
@@ -61,7 +61,7 @@ module Farce
61
61
  end
62
62
 
63
63
  def wait_for_change?(observed, deadline)
64
- timeout = deadline - Clock.now if deadline
64
+ timeout = Clock.remaining(deadline)
65
65
  return false if timeout && !timeout.positive?
66
66
  !!@changes.wait(observed, timeout:) { false }
67
67
  end
@@ -54,6 +54,8 @@ module Farce
54
54
  end
55
55
  end
56
56
 
57
+ def native_weak_maps? = true
58
+
57
59
  # define_method supplies the receiver. Rebinding first is unnecessary without
58
60
  # native Ractor isolation and can change the method context used by super.
59
61
  def prepare_method_definition(&definition) = definition
@@ -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
@@ -60,7 +60,6 @@ module Farce
60
60
  check_frozen!
61
61
  end
62
62
  change(delta)
63
- self
64
63
  end
65
64
 
66
65
  def decrement(delta = 1)
@@ -70,7 +69,6 @@ module Farce
70
69
  check_frozen!
71
70
  end
72
71
  change(-delta)
73
- self
74
72
  end
75
73
 
76
74
  alias value get
@@ -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
@@ -39,6 +39,15 @@ module Farce
39
39
 
40
40
  def native_ractors? = false
41
41
 
42
+ # Range#initialize rejects open endpoints on TruffleRuby, while Range.new accepts them.
43
+ def initialize_range(target, first, last, exclude_end)
44
+ if first.nil? || last.nil?
45
+ target.__send__(:initialize_copy, target.class.new(first, last, exclude_end))
46
+ else
47
+ target.__send__(:initialize, first, last, exclude_end)
48
+ end
49
+ end
50
+
42
51
  # @api private
43
52
  # TruffleRuby probes this additional hook before using _dump.
44
53
  def marshal_protocol_method?(name)
@@ -100,13 +100,15 @@ module Farce
100
100
  # @yield the block containing method definitions
101
101
  # @yieldreceiver [Module, Refinement] the module/refinement used for extending the target class.
102
102
  # @return [self] the extension instance
103
- def define(*targets, &)
103
+ # rubocop:disable-next Naming/BlockForwarding, Style/ArgumentsForwarding
104
+ def define(*targets, &block)
105
+ # JRuby rejects repeated anonymous block forwarding inside a block.
104
106
  @lock.synchronize do
105
107
  check_state!
106
108
  targets.each do |target|
107
- refine(target, &)
109
+ refine(target, &block)
108
110
  mixin = @definitions.store_if_absent(target) { Mixin.new(self, target) }
109
- mixin.module_eval(&)
111
+ mixin.module_eval(&block)
110
112
  mixin.install if installed?
111
113
  end
112
114
  end
@@ -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
@@ -7,5 +7,6 @@ require "farce" unless defined?(Farce::Abstract)
7
7
  require "sorted_set"
8
8
 
9
9
  module Farce
10
+ Farce.dedup.store(::SortedSet)
10
11
  Internal::Converter.define(::SortedSet, :SortedSet)
11
12
  end