Module: Farce::Abstract::Counter Abstract

Includes:
Value, Internal::Copyable, Internal::ValueSerialization, Internal::Inspect, Internal::MarshalSupport::Counter
Included in:
Counter, Local::Counter
Defined in:
lib/farce/abstract/counter.rb,
lib/farce/integrations/psych.rb

Overview

This module is abstract.

Common numeric interface and atomic operations for counters.

Note:

This is a module rather than a class so Counter can inherit directly from the native counter and avoid delegation overhead.

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Internal::ValueSerialization

#as_extended_json, #as_json, #to_bson, #to_bson_normalized_value, #to_cbor, #to_json, #to_msgpack

Methods included from Value

#blank?, #unwrap

Methods included from Internal::Copyable

#duplicable?

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing ⇒ BasicObject (private)



224
# File 'lib/farce/abstract/counter.rb', line 224

def method_missing(...)              = value.public_send(...)

Class Method Details

.included(base) ⇒ BasicObject



32
33
34
35
# File 'lib/farce/abstract/counter.rb', line 32

def self.included(base)
  Internal.prepare_mutable_numeric(base)
  super
end

Instance Method Details

#add(by = 1) ⇒ BasicObject



207
# File 'lib/farce/abstract/counter.rb', line 207

def add(...) = increment(...)

#decrement(by = 1) ⇒ Integer

Atomically decrement the counter by the given amount (default is 1).

Parameters:

  • by (Numeric, String, #to_int) (defaults to: 1) —

    The amount, converted to an Integer.

Returns:

  • (Integer) —

    The value produced by this atomic update.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/farce/abstract/counter.rb', line 27

module Counter
  include Internal::MarshalSupport::Counter
  include Internal::Copyable
  include Internal::Inspect

  def self.included(base)
    Internal.prepare_mutable_numeric(base)
    super
  end

  # Numeric's default copying returns self, which is unsuitable for mutable counters.
  define_method(:dup,   Kernel.instance_method(:dup))
  define_method(:clone, Kernel.instance_method(:clone))

  include Value
  include Internal::ValueSerialization

  # Pass the current value to the given block, replacing it with the block's result.
  # May retry if the value changes during the block's execution.
  #
  # Provided for compatibility with `Concurrent::AtomicFixnum`.
  #
  # @yield [count] Calculate the new value based on the current value.
  # @yieldparam count [Integer] the current value of the atomic fixnum.
  # @yieldreturn [Integer] the new value to replace the current value.
  # @return [Integer] the new value after the block has been applied.
  def update
    raise LocalJumpError, "no block given" unless block_given?
    while true
      current   = value
      new_value = yield(current)
      return new_value if compare_and_set(current, new_value)
    end
  end

  # Wait until the current value differs from the expected value.
  # @param expected [Integer] the value to wait to change from
  # @param timeout [Numeric, nil] the total seconds available
  # @yield called when the timeout expires
  # @return [Integer, BasicObject, nil] the changed value or timeout fallback
  def wait_until_changed(expected, timeout: nil)
    signal = change_signal
    Internal.with_timeout(timeout) do |_, deadline|
      observed = signal.generation
      current = value
      return current unless expected == current
      break unless signal.wait(observed, timeout: Clock.remaining(deadline))
    end
    yield if block_given?
  end

  # Wait until a block condition matches the current value.
  # One timeout budget covers all checks and waits. The block is not interrupted.
  # @yieldparam value [Integer] the current value
  # @yieldreturn [Boolean] whether the value matches
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  # @raise [LocalJumpError] if no block is given
  def wait_until(timeout: nil, &) = Internal.wait_until(self, timeout:, &)

  # Wait while the block returns a truthy value.
  # @yieldparam value [Integer] the current value
  # @yieldreturn [BasicObject] a truthy value to keep waiting, or nil or false to stop
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the value when the condition becomes false, or nil on timeout
  # @raise [LocalJumpError] if no block is given
  def wait_while(timeout: nil)
    raise LocalJumpError, "no block given" unless block_given?
    wait_until(timeout:) { |value| !yield(value) }
  end

  # Wait while `object === value` is true.
  # @param object [#===] the pattern to stop matching
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the first nonmatching value, or nil on timeout
  def wait_while_match(object, timeout: nil)
    wait_while(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
  end

  # (see #wait_until_changed)
  def wait_while_value(...) = wait_until_changed(...)

  # Wait until `object == value` is true.
  # Counters compare integer values by equality.
  # @param object [BasicObject] the value to compare with the current value
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_value(object, timeout: nil)
    wait_until(timeout:) { |value| object == value }
  end

  # Wait until `object === value` is true.
  # @param object [#===] the pattern to match
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_match(object, timeout: nil)
    wait_until(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
  end

  # Wait until the value is strictly below the limit.
  # @param limit [Numeric, String, #to_int] the upper bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_below(limit, timeout: nil)
    limit = Integer(limit)
    wait_until(timeout:) { |value| value < limit }
  end

  # Wait until the value is strictly above the floor.
  # @param floor [Numeric, String, #to_int] the lower bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_above(floor, timeout: nil)
    floor = Integer(floor)
    wait_until(timeout:) { |value| value > floor }
  end

  # Wait while the value is strictly above the limit.
  # @param limit [Numeric, String, #to_int] the upper bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the first value at or below the limit, or nil on timeout
  def wait_while_above(limit, timeout: nil)
    limit = Integer(limit)
    wait_until(timeout:) { |value| value <= limit }
  end

  # Wait while the value is strictly below the floor.
  # @param floor [Numeric, String, #to_int] the lower bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the first value at or above the floor, or nil on timeout
  def wait_while_below(floor, timeout: nil)
    floor = Integer(floor)
    wait_until(timeout:) { |value| value >= floor }
  end

  # Reset the counter to its initial value.
  # @return [self] Returns self for chaining.
  def reset
    self.value = initial
    self
  end

  # Increment the counter by one if its current value is below the limit.
  # The check and increment happen atomically.
  # @param [Numeric, String, #to_int] limit The upper bound, converted to an Integer.
  # @return [Boolean] true if the counter changed, otherwise false.
  def increment_if_below(limit) # rubocop:disable Naming/PredicateMethod
    limit   = Integer(limit)
    current = value

    Internal::Freeze.check(self)

    while current < limit
      return true if compare_and_set(current, current + 1)
      current = value
    end

    false
  end

  # Decrement the counter by one if its current value is above the floor.
  # The check and decrement happen atomically.
  # @param [Numeric, String, #to_int] floor The lower bound, converted to an Integer.
  # @return [Boolean] true if the counter changed, otherwise false.
  def decrement_if_above(floor) # rubocop:disable Naming/PredicateMethod
    floor   = Integer(floor)
    current = value

    Internal::Freeze.check(self)

    while current > floor
      return true if compare_and_set(current, current - 1)
      current = value
    end

    false
  end

  # @overload add(by = 1)
  #   (see #increment)
  def add(...) = increment(...)

  methods  = Integer.public_instance_methods - Object.public_instance_methods - [:singleton_method_added]
  methods += %i[+ - / * ** <=> coerce rationalize to_i to_int to_f to_c to_r to_s]
  methods.uniq!
  Internal.delegate(self, :value, *methods)

  # @api private
  def inspect_with(inspector)
    super do
      inspector.breakable
      inspector.object(value)
    end
  end

  private

  def method_missing(...)              = value.public_send(...)
  def respond_to_missing?(method, ...) = value.respond_to?(method)
end

#decrement_if_above(floor) ⇒ Boolean

Decrement the counter by one if its current value is above the floor. The check and decrement happen atomically.

Parameters:

  • floor (Numeric, String, #to_int) —

    The lower bound, converted to an Integer.

Returns:

  • (Boolean) —

    true if the counter changed, otherwise false.



191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/farce/abstract/counter.rb', line 191

def decrement_if_above(floor) # rubocop:disable Naming/PredicateMethod
  floor   = Integer(floor)
  current = value

  Internal::Freeze.check(self)

  while current > floor
    return true if compare_and_set(current, current - 1)
    current = value
  end

  false
end

#increment(by = 1) ⇒ Integer

Atomically increment the counter by the given amount (default is 1). Use the returned integer to allocate a sequence number without a separate read.

Parameters:

  • by (Numeric, String, #to_int) (defaults to: 1) —

    The amount, converted to an Integer.

Returns:

  • (Integer) —

    The value produced by this atomic update.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/farce/abstract/counter.rb', line 27

module Counter
  include Internal::MarshalSupport::Counter
  include Internal::Copyable
  include Internal::Inspect

  def self.included(base)
    Internal.prepare_mutable_numeric(base)
    super
  end

  # Numeric's default copying returns self, which is unsuitable for mutable counters.
  define_method(:dup,   Kernel.instance_method(:dup))
  define_method(:clone, Kernel.instance_method(:clone))

  include Value
  include Internal::ValueSerialization

  # Pass the current value to the given block, replacing it with the block's result.
  # May retry if the value changes during the block's execution.
  #
  # Provided for compatibility with `Concurrent::AtomicFixnum`.
  #
  # @yield [count] Calculate the new value based on the current value.
  # @yieldparam count [Integer] the current value of the atomic fixnum.
  # @yieldreturn [Integer] the new value to replace the current value.
  # @return [Integer] the new value after the block has been applied.
  def update
    raise LocalJumpError, "no block given" unless block_given?
    while true
      current   = value
      new_value = yield(current)
      return new_value if compare_and_set(current, new_value)
    end
  end

  # Wait until the current value differs from the expected value.
  # @param expected [Integer] the value to wait to change from
  # @param timeout [Numeric, nil] the total seconds available
  # @yield called when the timeout expires
  # @return [Integer, BasicObject, nil] the changed value or timeout fallback
  def wait_until_changed(expected, timeout: nil)
    signal = change_signal
    Internal.with_timeout(timeout) do |_, deadline|
      observed = signal.generation
      current = value
      return current unless expected == current
      break unless signal.wait(observed, timeout: Clock.remaining(deadline))
    end
    yield if block_given?
  end

  # Wait until a block condition matches the current value.
  # One timeout budget covers all checks and waits. The block is not interrupted.
  # @yieldparam value [Integer] the current value
  # @yieldreturn [Boolean] whether the value matches
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  # @raise [LocalJumpError] if no block is given
  def wait_until(timeout: nil, &) = Internal.wait_until(self, timeout:, &)

  # Wait while the block returns a truthy value.
  # @yieldparam value [Integer] the current value
  # @yieldreturn [BasicObject] a truthy value to keep waiting, or nil or false to stop
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the value when the condition becomes false, or nil on timeout
  # @raise [LocalJumpError] if no block is given
  def wait_while(timeout: nil)
    raise LocalJumpError, "no block given" unless block_given?
    wait_until(timeout:) { |value| !yield(value) }
  end

  # Wait while `object === value` is true.
  # @param object [#===] the pattern to stop matching
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the first nonmatching value, or nil on timeout
  def wait_while_match(object, timeout: nil)
    wait_while(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
  end

  # (see #wait_until_changed)
  def wait_while_value(...) = wait_until_changed(...)

  # Wait until `object == value` is true.
  # Counters compare integer values by equality.
  # @param object [BasicObject] the value to compare with the current value
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_value(object, timeout: nil)
    wait_until(timeout:) { |value| object == value }
  end

  # Wait until `object === value` is true.
  # @param object [#===] the pattern to match
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_match(object, timeout: nil)
    wait_until(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
  end

  # Wait until the value is strictly below the limit.
  # @param limit [Numeric, String, #to_int] the upper bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_below(limit, timeout: nil)
    limit = Integer(limit)
    wait_until(timeout:) { |value| value < limit }
  end

  # Wait until the value is strictly above the floor.
  # @param floor [Numeric, String, #to_int] the lower bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_above(floor, timeout: nil)
    floor = Integer(floor)
    wait_until(timeout:) { |value| value > floor }
  end

  # Wait while the value is strictly above the limit.
  # @param limit [Numeric, String, #to_int] the upper bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the first value at or below the limit, or nil on timeout
  def wait_while_above(limit, timeout: nil)
    limit = Integer(limit)
    wait_until(timeout:) { |value| value <= limit }
  end

  # Wait while the value is strictly below the floor.
  # @param floor [Numeric, String, #to_int] the lower bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the first value at or above the floor, or nil on timeout
  def wait_while_below(floor, timeout: nil)
    floor = Integer(floor)
    wait_until(timeout:) { |value| value >= floor }
  end

  # Reset the counter to its initial value.
  # @return [self] Returns self for chaining.
  def reset
    self.value = initial
    self
  end

  # Increment the counter by one if its current value is below the limit.
  # The check and increment happen atomically.
  # @param [Numeric, String, #to_int] limit The upper bound, converted to an Integer.
  # @return [Boolean] true if the counter changed, otherwise false.
  def increment_if_below(limit) # rubocop:disable Naming/PredicateMethod
    limit   = Integer(limit)
    current = value

    Internal::Freeze.check(self)

    while current < limit
      return true if compare_and_set(current, current + 1)
      current = value
    end

    false
  end

  # Decrement the counter by one if its current value is above the floor.
  # The check and decrement happen atomically.
  # @param [Numeric, String, #to_int] floor The lower bound, converted to an Integer.
  # @return [Boolean] true if the counter changed, otherwise false.
  def decrement_if_above(floor) # rubocop:disable Naming/PredicateMethod
    floor   = Integer(floor)
    current = value

    Internal::Freeze.check(self)

    while current > floor
      return true if compare_and_set(current, current - 1)
      current = value
    end

    false
  end

  # @overload add(by = 1)
  #   (see #increment)
  def add(...) = increment(...)

  methods  = Integer.public_instance_methods - Object.public_instance_methods - [:singleton_method_added]
  methods += %i[+ - / * ** <=> coerce rationalize to_i to_int to_f to_c to_r to_s]
  methods.uniq!
  Internal.delegate(self, :value, *methods)

  # @api private
  def inspect_with(inspector)
    super do
      inspector.breakable
      inspector.object(value)
    end
  end

  private

  def method_missing(...)              = value.public_send(...)
  def respond_to_missing?(method, ...) = value.respond_to?(method)
end

#increment_if_below(limit) ⇒ Boolean

Increment the counter by one if its current value is below the limit. The check and increment happen atomically.

Parameters:

  • limit (Numeric, String, #to_int) —

    The upper bound, converted to an Integer.

Returns:

  • (Boolean) —

    true if the counter changed, otherwise false.



173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/farce/abstract/counter.rb', line 173

def increment_if_below(limit) # rubocop:disable Naming/PredicateMethod
  limit   = Integer(limit)
  current = value

  Internal::Freeze.check(self)

  while current < limit
    return true if compare_and_set(current, current + 1)
    current = value
  end

  false
end

#initial ⇒ Integer

Returns The initial value retained by reset and copies.

Returns:

  • (Integer) —

    The initial value retained by reset and copies.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/farce/abstract/counter.rb', line 27

module Counter
  include Internal::MarshalSupport::Counter
  include Internal::Copyable
  include Internal::Inspect

  def self.included(base)
    Internal.prepare_mutable_numeric(base)
    super
  end

  # Numeric's default copying returns self, which is unsuitable for mutable counters.
  define_method(:dup,   Kernel.instance_method(:dup))
  define_method(:clone, Kernel.instance_method(:clone))

  include Value
  include Internal::ValueSerialization

  # Pass the current value to the given block, replacing it with the block's result.
  # May retry if the value changes during the block's execution.
  #
  # Provided for compatibility with `Concurrent::AtomicFixnum`.
  #
  # @yield [count] Calculate the new value based on the current value.
  # @yieldparam count [Integer] the current value of the atomic fixnum.
  # @yieldreturn [Integer] the new value to replace the current value.
  # @return [Integer] the new value after the block has been applied.
  def update
    raise LocalJumpError, "no block given" unless block_given?
    while true
      current   = value
      new_value = yield(current)
      return new_value if compare_and_set(current, new_value)
    end
  end

  # Wait until the current value differs from the expected value.
  # @param expected [Integer] the value to wait to change from
  # @param timeout [Numeric, nil] the total seconds available
  # @yield called when the timeout expires
  # @return [Integer, BasicObject, nil] the changed value or timeout fallback
  def wait_until_changed(expected, timeout: nil)
    signal = change_signal
    Internal.with_timeout(timeout) do |_, deadline|
      observed = signal.generation
      current = value
      return current unless expected == current
      break unless signal.wait(observed, timeout: Clock.remaining(deadline))
    end
    yield if block_given?
  end

  # Wait until a block condition matches the current value.
  # One timeout budget covers all checks and waits. The block is not interrupted.
  # @yieldparam value [Integer] the current value
  # @yieldreturn [Boolean] whether the value matches
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  # @raise [LocalJumpError] if no block is given
  def wait_until(timeout: nil, &) = Internal.wait_until(self, timeout:, &)

  # Wait while the block returns a truthy value.
  # @yieldparam value [Integer] the current value
  # @yieldreturn [BasicObject] a truthy value to keep waiting, or nil or false to stop
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the value when the condition becomes false, or nil on timeout
  # @raise [LocalJumpError] if no block is given
  def wait_while(timeout: nil)
    raise LocalJumpError, "no block given" unless block_given?
    wait_until(timeout:) { |value| !yield(value) }
  end

  # Wait while `object === value` is true.
  # @param object [#===] the pattern to stop matching
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the first nonmatching value, or nil on timeout
  def wait_while_match(object, timeout: nil)
    wait_while(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
  end

  # (see #wait_until_changed)
  def wait_while_value(...) = wait_until_changed(...)

  # Wait until `object == value` is true.
  # Counters compare integer values by equality.
  # @param object [BasicObject] the value to compare with the current value
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_value(object, timeout: nil)
    wait_until(timeout:) { |value| object == value }
  end

  # Wait until `object === value` is true.
  # @param object [#===] the pattern to match
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_match(object, timeout: nil)
    wait_until(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
  end

  # Wait until the value is strictly below the limit.
  # @param limit [Numeric, String, #to_int] the upper bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_below(limit, timeout: nil)
    limit = Integer(limit)
    wait_until(timeout:) { |value| value < limit }
  end

  # Wait until the value is strictly above the floor.
  # @param floor [Numeric, String, #to_int] the lower bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_above(floor, timeout: nil)
    floor = Integer(floor)
    wait_until(timeout:) { |value| value > floor }
  end

  # Wait while the value is strictly above the limit.
  # @param limit [Numeric, String, #to_int] the upper bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the first value at or below the limit, or nil on timeout
  def wait_while_above(limit, timeout: nil)
    limit = Integer(limit)
    wait_until(timeout:) { |value| value <= limit }
  end

  # Wait while the value is strictly below the floor.
  # @param floor [Numeric, String, #to_int] the lower bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the first value at or above the floor, or nil on timeout
  def wait_while_below(floor, timeout: nil)
    floor = Integer(floor)
    wait_until(timeout:) { |value| value >= floor }
  end

  # Reset the counter to its initial value.
  # @return [self] Returns self for chaining.
  def reset
    self.value = initial
    self
  end

  # Increment the counter by one if its current value is below the limit.
  # The check and increment happen atomically.
  # @param [Numeric, String, #to_int] limit The upper bound, converted to an Integer.
  # @return [Boolean] true if the counter changed, otherwise false.
  def increment_if_below(limit) # rubocop:disable Naming/PredicateMethod
    limit   = Integer(limit)
    current = value

    Internal::Freeze.check(self)

    while current < limit
      return true if compare_and_set(current, current + 1)
      current = value
    end

    false
  end

  # Decrement the counter by one if its current value is above the floor.
  # The check and decrement happen atomically.
  # @param [Numeric, String, #to_int] floor The lower bound, converted to an Integer.
  # @return [Boolean] true if the counter changed, otherwise false.
  def decrement_if_above(floor) # rubocop:disable Naming/PredicateMethod
    floor   = Integer(floor)
    current = value

    Internal::Freeze.check(self)

    while current > floor
      return true if compare_and_set(current, current - 1)
      current = value
    end

    false
  end

  # @overload add(by = 1)
  #   (see #increment)
  def add(...) = increment(...)

  methods  = Integer.public_instance_methods - Object.public_instance_methods - [:singleton_method_added]
  methods += %i[+ - / * ** <=> coerce rationalize to_i to_int to_f to_c to_r to_s]
  methods.uniq!
  Internal.delegate(self, :value, *methods)

  # @api private
  def inspect_with(inspector)
    super do
      inspector.breakable
      inspector.object(value)
    end
  end

  private

  def method_missing(...)              = value.public_send(...)
  def respond_to_missing?(method, ...) = value.respond_to?(method)
end

#reset ⇒ self

Reset the counter to its initial value.

Returns:

  • (self) —

    Returns self for chaining.



164
165
166
167
# File 'lib/farce/abstract/counter.rb', line 164

def reset
  self.value = initial
  self
end

#update {|count| ... } ⇒ Integer

Pass the current value to the given block, replacing it with the block's result. May retry if the value changes during the block's execution.

Provided for compatibility with Concurrent::AtomicFixnum.

Yields:

  • (count) —

    Calculate the new value based on the current value.

Yield Parameters:

  • count (Integer) —

    the current value of the atomic fixnum.

Yield Returns:

  • (Integer) —

    the new value to replace the current value.

Returns:

  • (Integer) —

    the new value after the block has been applied.

Raises:

  • (LocalJumpError)


53
54
55
56
57
58
59
60
# File 'lib/farce/abstract/counter.rb', line 53

def update
  raise LocalJumpError, "no block given" unless block_given?
  while true
    current   = value
    new_value = yield(current)
    return new_value if compare_and_set(current, new_value)
  end
end

#value ⇒ Integer

Returns The current value of the counter.

Returns:

  • (Integer) —

    The current value of the counter.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/farce/abstract/counter.rb', line 27

module Counter
  include Internal::MarshalSupport::Counter
  include Internal::Copyable
  include Internal::Inspect

  def self.included(base)
    Internal.prepare_mutable_numeric(base)
    super
  end

  # Numeric's default copying returns self, which is unsuitable for mutable counters.
  define_method(:dup,   Kernel.instance_method(:dup))
  define_method(:clone, Kernel.instance_method(:clone))

  include Value
  include Internal::ValueSerialization

  # Pass the current value to the given block, replacing it with the block's result.
  # May retry if the value changes during the block's execution.
  #
  # Provided for compatibility with `Concurrent::AtomicFixnum`.
  #
  # @yield [count] Calculate the new value based on the current value.
  # @yieldparam count [Integer] the current value of the atomic fixnum.
  # @yieldreturn [Integer] the new value to replace the current value.
  # @return [Integer] the new value after the block has been applied.
  def update
    raise LocalJumpError, "no block given" unless block_given?
    while true
      current   = value
      new_value = yield(current)
      return new_value if compare_and_set(current, new_value)
    end
  end

  # Wait until the current value differs from the expected value.
  # @param expected [Integer] the value to wait to change from
  # @param timeout [Numeric, nil] the total seconds available
  # @yield called when the timeout expires
  # @return [Integer, BasicObject, nil] the changed value or timeout fallback
  def wait_until_changed(expected, timeout: nil)
    signal = change_signal
    Internal.with_timeout(timeout) do |_, deadline|
      observed = signal.generation
      current = value
      return current unless expected == current
      break unless signal.wait(observed, timeout: Clock.remaining(deadline))
    end
    yield if block_given?
  end

  # Wait until a block condition matches the current value.
  # One timeout budget covers all checks and waits. The block is not interrupted.
  # @yieldparam value [Integer] the current value
  # @yieldreturn [Boolean] whether the value matches
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  # @raise [LocalJumpError] if no block is given
  def wait_until(timeout: nil, &) = Internal.wait_until(self, timeout:, &)

  # Wait while the block returns a truthy value.
  # @yieldparam value [Integer] the current value
  # @yieldreturn [BasicObject] a truthy value to keep waiting, or nil or false to stop
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the value when the condition becomes false, or nil on timeout
  # @raise [LocalJumpError] if no block is given
  def wait_while(timeout: nil)
    raise LocalJumpError, "no block given" unless block_given?
    wait_until(timeout:) { |value| !yield(value) }
  end

  # Wait while `object === value` is true.
  # @param object [#===] the pattern to stop matching
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the first nonmatching value, or nil on timeout
  def wait_while_match(object, timeout: nil)
    wait_while(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
  end

  # (see #wait_until_changed)
  def wait_while_value(...) = wait_until_changed(...)

  # Wait until `object == value` is true.
  # Counters compare integer values by equality.
  # @param object [BasicObject] the value to compare with the current value
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_value(object, timeout: nil)
    wait_until(timeout:) { |value| object == value }
  end

  # Wait until `object === value` is true.
  # @param object [#===] the pattern to match
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_match(object, timeout: nil)
    wait_until(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
  end

  # Wait until the value is strictly below the limit.
  # @param limit [Numeric, String, #to_int] the upper bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_below(limit, timeout: nil)
    limit = Integer(limit)
    wait_until(timeout:) { |value| value < limit }
  end

  # Wait until the value is strictly above the floor.
  # @param floor [Numeric, String, #to_int] the lower bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the matching value, or nil on timeout
  def wait_until_above(floor, timeout: nil)
    floor = Integer(floor)
    wait_until(timeout:) { |value| value > floor }
  end

  # Wait while the value is strictly above the limit.
  # @param limit [Numeric, String, #to_int] the upper bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the first value at or below the limit, or nil on timeout
  def wait_while_above(limit, timeout: nil)
    limit = Integer(limit)
    wait_until(timeout:) { |value| value <= limit }
  end

  # Wait while the value is strictly below the floor.
  # @param floor [Numeric, String, #to_int] the lower bound, converted to an Integer
  # @param timeout [Numeric, nil] the total seconds available
  # @return [Integer, nil] the first value at or above the floor, or nil on timeout
  def wait_while_below(floor, timeout: nil)
    floor = Integer(floor)
    wait_until(timeout:) { |value| value >= floor }
  end

  # Reset the counter to its initial value.
  # @return [self] Returns self for chaining.
  def reset
    self.value = initial
    self
  end

  # Increment the counter by one if its current value is below the limit.
  # The check and increment happen atomically.
  # @param [Numeric, String, #to_int] limit The upper bound, converted to an Integer.
  # @return [Boolean] true if the counter changed, otherwise false.
  def increment_if_below(limit) # rubocop:disable Naming/PredicateMethod
    limit   = Integer(limit)
    current = value

    Internal::Freeze.check(self)

    while current < limit
      return true if compare_and_set(current, current + 1)
      current = value
    end

    false
  end

  # Decrement the counter by one if its current value is above the floor.
  # The check and decrement happen atomically.
  # @param [Numeric, String, #to_int] floor The lower bound, converted to an Integer.
  # @return [Boolean] true if the counter changed, otherwise false.
  def decrement_if_above(floor) # rubocop:disable Naming/PredicateMethod
    floor   = Integer(floor)
    current = value

    Internal::Freeze.check(self)

    while current > floor
      return true if compare_and_set(current, current - 1)
      current = value
    end

    false
  end

  # @overload add(by = 1)
  #   (see #increment)
  def add(...) = increment(...)

  methods  = Integer.public_instance_methods - Object.public_instance_methods - [:singleton_method_added]
  methods += %i[+ - / * ** <=> coerce rationalize to_i to_int to_f to_c to_r to_s]
  methods.uniq!
  Internal.delegate(self, :value, *methods)

  # @api private
  def inspect_with(inspector)
    super do
      inspector.breakable
      inspector.object(value)
    end
  end

  private

  def method_missing(...)              = value.public_send(...)
  def respond_to_missing?(method, ...) = value.respond_to?(method)
end

#wait_until(timeout: nil) {|value| ... } ⇒ Integer?

Wait until a block condition matches the current value. One timeout budget covers all checks and waits. The block is not interrupted.

Parameters:

  • timeout (Numeric, nil) (defaults to: nil) —

    the total seconds available

Yield Parameters:

  • value (Integer) —

    the current value

Yield Returns:

  • (Boolean) —

    whether the value matches

Returns:

  • (Integer, nil) —

    the matching value, or nil on timeout

Raises:

  • (LocalJumpError) —

    if no block is given



85
# File 'lib/farce/abstract/counter.rb', line 85

def wait_until(timeout: nil, &) = Internal.wait_until(self, timeout:, &)

#wait_until_above(floor, timeout: nil) ⇒ Integer?

Wait until the value is strictly above the floor.

Parameters:

  • floor (Numeric, String, #to_int) —

    the lower bound, converted to an Integer

  • timeout (Numeric, nil) (defaults to: nil) —

    the total seconds available

Returns:

  • (Integer, nil) —

    the matching value, or nil on timeout



139
140
141
142
# File 'lib/farce/abstract/counter.rb', line 139

def wait_until_above(floor, timeout: nil)
  floor = Integer(floor)
  wait_until(timeout:) { |value| value > floor }
end

#wait_until_below(limit, timeout: nil) ⇒ Integer?

Wait until the value is strictly below the limit.

Parameters:

  • limit (Numeric, String, #to_int) —

    the upper bound, converted to an Integer

  • timeout (Numeric, nil) (defaults to: nil) —

    the total seconds available

Returns:

  • (Integer, nil) —

    the matching value, or nil on timeout



130
131
132
133
# File 'lib/farce/abstract/counter.rb', line 130

def wait_until_below(limit, timeout: nil)
  limit = Integer(limit)
  wait_until(timeout:) { |value| value < limit }
end

#wait_until_changed(expected, timeout: nil) { ... } ⇒ Integer, ...

Wait until the current value differs from the expected value.

Parameters:

  • expected (Integer) —

    the value to wait to change from

  • timeout (Numeric, nil) (defaults to: nil) —

    the total seconds available

Yields:

  • called when the timeout expires

Returns:

  • (Integer, BasicObject, nil) —

    the changed value or timeout fallback



67
68
69
70
71
72
73
74
75
76
# File 'lib/farce/abstract/counter.rb', line 67

def wait_until_changed(expected, timeout: nil)
  signal = change_signal
  Internal.with_timeout(timeout) do |_, deadline|
    observed = signal.generation
    current = value
    return current unless expected == current
    break unless signal.wait(observed, timeout: Clock.remaining(deadline))
  end
  yield if block_given?
end

#wait_until_match(object, timeout: nil) ⇒ Integer?

Wait until object === value is true.

Parameters:

  • object (#===) —

    the pattern to match

  • timeout (Numeric, nil) (defaults to: nil) —

    the total seconds available

Returns:

  • (Integer, nil) —

    the matching value, or nil on timeout



122
123
124
# File 'lib/farce/abstract/counter.rb', line 122

def wait_until_match(object, timeout: nil)
  wait_until(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
end

#wait_until_value(object, timeout: nil) ⇒ Integer?

Wait until object == value is true. Counters compare integer values by equality.

Parameters:

  • object (BasicObject) —

    the value to compare with the current value

  • timeout (Numeric, nil) (defaults to: nil) —

    the total seconds available

Returns:

  • (Integer, nil) —

    the matching value, or nil on timeout



114
115
116
# File 'lib/farce/abstract/counter.rb', line 114

def wait_until_value(object, timeout: nil)
  wait_until(timeout:) { |value| object == value }
end

#wait_while(timeout: nil) {|value| ... } ⇒ Integer?

Wait while the block returns a truthy value.

Parameters:

  • timeout (Numeric, nil) (defaults to: nil) —

    the total seconds available

Yield Parameters:

  • value (Integer) —

    the current value

Yield Returns:

  • (BasicObject) —

    a truthy value to keep waiting, or nil or false to stop

Returns:

  • (Integer, nil) —

    the value when the condition becomes false, or nil on timeout

Raises:

  • (LocalJumpError) —

    if no block is given



93
94
95
96
# File 'lib/farce/abstract/counter.rb', line 93

def wait_while(timeout: nil)
  raise LocalJumpError, "no block given" unless block_given?
  wait_until(timeout:) { |value| !yield(value) }
end

#wait_while_above(limit, timeout: nil) ⇒ Integer?

Wait while the value is strictly above the limit.

Parameters:

  • limit (Numeric, String, #to_int) —

    the upper bound, converted to an Integer

  • timeout (Numeric, nil) (defaults to: nil) —

    the total seconds available

Returns:

  • (Integer, nil) —

    the first value at or below the limit, or nil on timeout



148
149
150
151
# File 'lib/farce/abstract/counter.rb', line 148

def wait_while_above(limit, timeout: nil)
  limit = Integer(limit)
  wait_until(timeout:) { |value| value <= limit }
end

#wait_while_below(floor, timeout: nil) ⇒ Integer?

Wait while the value is strictly below the floor.

Parameters:

  • floor (Numeric, String, #to_int) —

    the lower bound, converted to an Integer

  • timeout (Numeric, nil) (defaults to: nil) —

    the total seconds available

Returns:

  • (Integer, nil) —

    the first value at or above the floor, or nil on timeout



157
158
159
160
# File 'lib/farce/abstract/counter.rb', line 157

def wait_while_below(floor, timeout: nil)
  floor = Integer(floor)
  wait_until(timeout:) { |value| value >= floor }
end

#wait_while_match(object, timeout: nil) ⇒ Integer?

Wait while object === value is true.

Parameters:

  • object (#===) —

    the pattern to stop matching

  • timeout (Numeric, nil) (defaults to: nil) —

    the total seconds available

Returns:

  • (Integer, nil) —

    the first nonmatching value, or nil on timeout



102
103
104
# File 'lib/farce/abstract/counter.rb', line 102

def wait_while_match(object, timeout: nil)
  wait_while(timeout:) { |value| object === value } # rubocop:disable Style/CaseEquality
end

#wait_while_value { ... } ⇒ Integer, ...

Wait until the current value differs from the expected value.

Parameters:

  • expected (Integer) —

    the value to wait to change from

  • timeout (Numeric, nil) —

    the total seconds available

Yields:

  • called when the timeout expires

Returns:

  • (Integer, BasicObject, nil) —

    the changed value or timeout fallback



107
# File 'lib/farce/abstract/counter.rb', line 107

def wait_while_value(...) = wait_until_changed(...)