Class: Farce::Atom

Inherits:
Farce::Abstract::Atom show all
Includes:
Shareable::Delegated
Defined in:
lib/farce/atom.rb,
lib/farce/integrations/psych.rb,
lib/farce/integrations/active_support/blank.rb

Overview

A Ractor-shareable atomic reference that strongly retains its current value and can hold non-shareable values.

Values are transferred according to the atom's default #mode, or an operation-specific mode where supported. Values returned by the atom are automatically unwrapped. Updates are serialized, and comparison operations compare the stored values without claiming or opening their envelopes when possible.

Examples:

Creating a new atom

atom = Farce::Atom.new("initial value")
atom.value # => "initial value"

# atoms are ractor-shareable
Ractor.new(atom) do |atom|
  atom.update { |current| current + " updated" } # => "initial value updated"
end

# waits for the other ractor to perform its update
atom.wait_until_changed "initial value"

Updating a counter atomically

# Note: Farce::Counter would have better performance
counter = Farce::Atom.new(0)
counter.update { |count| count + 1 } # => 1
counter.compare_and_set(1, 2)        # => true
counter.value                        # => 2

Automatically making values shareable

atom = Farce::Atom.new([], mode: :make_shareable)
atom.update { |items| items + [:job] } # => [:job]
atom.value                    # => [:job]
Ractor.shareable?(atom.value) # => true

ActiveSupport Integration collapse

Methods included from Internal::ValueSerialization

#as_json, #to_msgpack

Methods included from Internal::Copyable

#duplicable?

Instance Method Summary collapse

Methods included from Shareable

#ractor_shareable?

Methods inherited from Farce::Abstract::Atom

#wait_until, #wait_until_match, #wait_until_value, #wait_while, #wait_while_match, #wait_while_value

Methods included from Internal::ValueSerialization

#as_extended_json, #to_bson, #to_bson_normalized_value, #to_cbor, #to_json

Methods included from Farce::Abstract::Value

#unwrap

Constructor Details

#initialize(value = nil, compare_by_identity: false, mode: :copy) ⇒ Atom

Valid modes are:

  • :copy - The value will be copied between Ractors. This is the default mode.
  • :make_shareable - The value will be made Ractor-shareable using Ractor.make_shareable.
  • :move - The value will be moved between Ractors. This saves memory compared to copying, and supports values that can't be copied but moved (like IO objects). However, the value will no longer be accessible on the Ractor that pushed it.
  • :mutable - A Mutable instance will be created for the value. This isn't done recursively and thus will fail for nested unshareable values.
  • :local - The value will be kept local to the Ractor that pushed it. Another ractor trying to receive it will get an error. Useful for usage contained within a single Ractor.
  • :proxy - The value will be wrapped in a Farce::Proxy that executes calls in the original Ractor.
  • :raise - An error will be raised if the value is not Ractor-shareable. Useful for enforcing shareability.
  • :dedup - The value will be deduplicated using Farce.dedup, then made Ractor-shareable. This may update and freeze the original. Already-shareable values pass through unchanged.
  • :shareable_copy - The value will be copied and the copy will be made Ractor-shareable.

Parameters:

  • value (BasicObject, nil) (defaults to: nil) —

    the initial value

  • compare_by_identity (Boolean) (defaults to: false) —

    whether comparisons use object identity instead of equality

  • mode (Symbol) (defaults to: :copy) —

    the default mode used to transfer values between Ractors



47
48
49
50
51
52
53
54
55
56
# File 'lib/farce/atom.rb', line 47

def initialize(value = nil, compare_by_identity: false, mode: :copy)
  unless compare_by_identity == true || compare_by_identity == false
    raise ArgumentError, "compare_by_identity must be a boolean"
  end

  @manager             = ModeManager.new(mode:)
  @compare_by_identity = compare_by_identity
  @atom                = Internal::Atom.new(wrap_value(value), compare_by_identity: true)
  super()
end

Instance Method Details

#blank? ⇒ Boolean

Note:

This methods is only available if ActiveSupport has been loaded.

Returns true if the atom's value is blank, false otherwise.

Returns:

  • (Boolean) —

    true if the atom's value is blank, false otherwise.



47
48
49
50
# File 'lib/farce/integrations/active_support/blank.rb', line 47

def blank?
  current = internal_atom.value
  NIL_VALUE.equal?(current) || current.blank?
end

#compare_and_set(expected, new_value, mode: nil, timeout: nil) ⇒ Boolean

Atomically replace the current value if it matches the expected value. Valid modes are:

  • :copy - The value will be copied between Ractors. This is the default mode.
  • :make_shareable - The value will be made Ractor-shareable using Ractor.make_shareable.
  • :move - The value will be moved between Ractors. This saves memory compared to copying, and supports values that can't be copied but moved (like IO objects). However, the value will no longer be accessible on the Ractor that pushed it.
  • :mutable - A Mutable instance will be created for the value. This isn't done recursively and thus will fail for nested unshareable values.
  • :local - The value will be kept local to the Ractor that pushed it. Another ractor trying to receive it will get an error. Useful for usage contained within a single Ractor.
  • :proxy - The value will be wrapped in a Farce::Proxy that executes calls in the original Ractor.
  • :raise - An error will be raised if the value is not Ractor-shareable. Useful for enforcing shareability.
  • :dedup - The value will be deduplicated using Farce.dedup, then made Ractor-shareable. This may update and freeze the original. Already-shareable values pass through unchanged.
  • :shareable_copy - The value will be copied and the copy will be made Ractor-shareable.

Parameters:

  • expected (BasicObject, nil) —

    the value to compare with the current value

  • new_value (BasicObject, nil) —

    the replacement value

  • mode (Symbol, nil) (defaults to: nil) —

    the replacement's transfer mode, or nil to use the atom's default mode

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

    the maximum number of seconds to wait

Returns:

  • (Boolean) —

    whether the value was replaced



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/farce/atom.rb', line 160

def compare_and_set(expected, new_value, mode: nil, timeout: nil)
  check_frozen!
  expected = wrap_comparison(expected)
  matched  = false

  @atom.update(timeout:) do |current|
    equal = values_equal?(current, expected)
    check_frozen!
    next current unless equal

    matched = true
    wrap_value(new_value, mode:)
  end
  matched
end

#compare_by_identity? ⇒ Boolean

Whether comparisons use object identity instead of equality.

Returns:

  • (Boolean)


87
# File 'lib/farce/atom.rb', line 87

def compare_by_identity? = @compare_by_identity

#get(timeout: nil) { ... } ⇒ BasicObject?

Return the current value, waiting for any update in progress.

Parameters:

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

    the maximum number of seconds to wait

Yields:

  • called when the timeout expires

Returns:

  • (BasicObject, nil) —

    the current value or the fallback result



104
# File 'lib/farce/atom.rb', line 104

def get(timeout: nil, &) = unwrap_result(@atom.get(timeout:) { TIMED_OUT }, &)

#mode ⇒ Symbol

The default mode used to transfer values between Ractors.

Returns:

  • (Symbol)


83
# File 'lib/farce/atom.rb', line 83

def mode = @manager.mode

#store(new_value, mode: nil, timeout: nil) { ... } ⇒ BasicObject?

Store a new value, waiting for any update in progress. Valid modes are:

  • :copy - The value will be copied between Ractors. This is the default mode.
  • :make_shareable - The value will be made Ractor-shareable using Ractor.make_shareable.
  • :move - The value will be moved between Ractors. This saves memory compared to copying, and supports values that can't be copied but moved (like IO objects). However, the value will no longer be accessible on the Ractor that pushed it.
  • :mutable - A Mutable instance will be created for the value. This isn't done recursively and thus will fail for nested unshareable values.
  • :local - The value will be kept local to the Ractor that pushed it. Another ractor trying to receive it will get an error. Useful for usage contained within a single Ractor.
  • :proxy - The value will be wrapped in a Farce::Proxy that executes calls in the original Ractor.
  • :raise - An error will be raised if the value is not Ractor-shareable. Useful for enforcing shareability.
  • :dedup - The value will be deduplicated using Farce.dedup, then made Ractor-shareable. This may update and freeze the original. Already-shareable values pass through unchanged.
  • :shareable_copy - The value will be copied and the copy will be made Ractor-shareable.

Parameters:

  • new_value (BasicObject, nil) —

    the new value

  • mode (Symbol, nil) (defaults to: nil) —

    the transfer mode, or nil to use the atom's default mode

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

    the maximum number of seconds to wait

Yields:

  • called when the timeout expires

Returns:

  • (BasicObject, nil) —

    the stored value or the fallback result



113
114
115
116
117
# File 'lib/farce/atom.rb', line 113

def store(new_value, mode: nil, timeout: nil, &)
  check_frozen!
  new_value = wrap_value(new_value, mode:)
  unwrap_result(@atom.store(new_value, timeout:) { TIMED_OUT }, &)
end

#store_if_absent(mode: nil, timeout: nil) { ... } ⇒ BasicObject?

Compute and store a value if the current value is nil. Valid modes are:

  • :copy - The value will be copied between Ractors. This is the default mode.
  • :make_shareable - The value will be made Ractor-shareable using Ractor.make_shareable.
  • :move - The value will be moved between Ractors. This saves memory compared to copying, and supports values that can't be copied but moved (like IO objects). However, the value will no longer be accessible on the Ractor that pushed it.
  • :mutable - A Mutable instance will be created for the value. This isn't done recursively and thus will fail for nested unshareable values.
  • :local - The value will be kept local to the Ractor that pushed it. Another ractor trying to receive it will get an error. Useful for usage contained within a single Ractor.
  • :proxy - The value will be wrapped in a Farce::Proxy that executes calls in the original Ractor.
  • :raise - An error will be raised if the value is not Ractor-shareable. Useful for enforcing shareability.
  • :dedup - The value will be deduplicated using Farce.dedup, then made Ractor-shareable. This may update and freeze the original. Already-shareable values pass through unchanged.
  • :shareable_copy - The value will be copied and the copy will be made Ractor-shareable.

Parameters:

  • mode (Symbol, nil) (defaults to: nil) —

    the transfer mode, or nil to use the atom's default mode

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

    the maximum number of seconds to wait

Yields:

  • computes the value to store when the current value is nil

Yield Returns:

  • (BasicObject, nil) —

    the value to store

Returns:

  • (BasicObject, nil) —

    the current or newly stored value, or nil when the timeout expires

Raises:

  • (LocalJumpError)


139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/farce/atom.rb', line 139

def store_if_absent(mode: nil, timeout: nil)
  raise LocalJumpError, "no block given" unless block_given?
  check_frozen!

  result = @atom.update(timeout:) do |current|
    next current unless NIL_VALUE.equal?(current)

    value = yield
    check_frozen!
    wrap_value(value, mode:)
  end
  unwrap_value(result)
end

#swap(new_value, mode: nil, timeout: nil) { ... } ⇒ BasicObject?

Replace the current value and return the previous value. Valid modes are:

  • :copy - The value will be copied between Ractors. This is the default mode.
  • :make_shareable - The value will be made Ractor-shareable using Ractor.make_shareable.
  • :move - The value will be moved between Ractors. This saves memory compared to copying, and supports values that can't be copied but moved (like IO objects). However, the value will no longer be accessible on the Ractor that pushed it.
  • :mutable - A Mutable instance will be created for the value. This isn't done recursively and thus will fail for nested unshareable values.
  • :local - The value will be kept local to the Ractor that pushed it. Another ractor trying to receive it will get an error. Useful for usage contained within a single Ractor.
  • :proxy - The value will be wrapped in a Farce::Proxy that executes calls in the original Ractor.
  • :raise - An error will be raised if the value is not Ractor-shareable. Useful for enforcing shareability.
  • :dedup - The value will be deduplicated using Farce.dedup, then made Ractor-shareable. This may update and freeze the original. Already-shareable values pass through unchanged.
  • :shareable_copy - The value will be copied and the copy will be made Ractor-shareable.

Parameters:

  • new_value (BasicObject, nil) —

    the new value

  • mode (Symbol, nil) (defaults to: nil) —

    the transfer mode, or nil to use the atom's default mode

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

    the maximum number of seconds to wait

Yields:

  • called when the timeout expires

Returns:

  • (BasicObject, nil) —

    the previous value or the fallback result



126
127
128
129
130
# File 'lib/farce/atom.rb', line 126

def swap(new_value, mode: nil, timeout: nil, &)
  check_frozen!
  new_value = wrap_value(new_value, mode:)
  unwrap_result(@atom.swap(new_value, timeout:) { TIMED_OUT }, &)
end

#update(mode: nil, timeout: nil) {|current| ... } ⇒ BasicObject?

Atomically replace the current value with the result of a block. Valid modes are:

  • :copy - The value will be copied between Ractors. This is the default mode.
  • :make_shareable - The value will be made Ractor-shareable using Ractor.make_shareable.
  • :move - The value will be moved between Ractors. This saves memory compared to copying, and supports values that can't be copied but moved (like IO objects). However, the value will no longer be accessible on the Ractor that pushed it.
  • :mutable - A Mutable instance will be created for the value. This isn't done recursively and thus will fail for nested unshareable values.
  • :local - The value will be kept local to the Ractor that pushed it. Another ractor trying to receive it will get an error. Useful for usage contained within a single Ractor.
  • :proxy - The value will be wrapped in a Farce::Proxy that executes calls in the original Ractor.
  • :raise - An error will be raised if the value is not Ractor-shareable. Useful for enforcing shareability.
  • :dedup - The value will be deduplicated using Farce.dedup, then made Ractor-shareable. This may update and freeze the original. Already-shareable values pass through unchanged.
  • :shareable_copy - The value will be copied and the copy will be made Ractor-shareable.

Parameters:

  • mode (Symbol, nil) (defaults to: nil) —

    the result's transfer mode, or nil to use the atom's default mode

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

    the maximum number of seconds to wait

Yields:

  • receives the current value and computes its replacement

Yield Parameters:

  • current (BasicObject, nil) —

    the current value

Yield Returns:

  • (BasicObject, nil) —

    the replacement value

Returns:

  • (BasicObject, nil) —

    the replacement value, or nil when the timeout expires

Raises:

  • (LocalJumpError)


184
185
186
187
188
189
190
191
192
193
# File 'lib/farce/atom.rb', line 184

def update(mode: nil, timeout: nil)
  raise LocalJumpError, "no block given" unless block_given?
  check_frozen!
  result = @atom.update(timeout:) do |current|
    value = yield(unwrap_value(current))
    check_frozen!
    wrap_value(value, mode:)
  end
  unwrap_value(result)
end

#upsert(initial_value, mode: nil, timeout: nil) {|current| ... } ⇒ BasicObject?

Store an initial value if the current value is nil, otherwise replace it with the result of a block. Valid modes are:

  • :copy - The value will be copied between Ractors. This is the default mode.
  • :make_shareable - The value will be made Ractor-shareable using Ractor.make_shareable.
  • :move - The value will be moved between Ractors. This saves memory compared to copying, and supports values that can't be copied but moved (like IO objects). However, the value will no longer be accessible on the Ractor that pushed it.
  • :mutable - A Mutable instance will be created for the value. This isn't done recursively and thus will fail for nested unshareable values.
  • :local - The value will be kept local to the Ractor that pushed it. Another ractor trying to receive it will get an error. Useful for usage contained within a single Ractor.
  • :proxy - The value will be wrapped in a Farce::Proxy that executes calls in the original Ractor.
  • :raise - An error will be raised if the value is not Ractor-shareable. Useful for enforcing shareability.
  • :dedup - The value will be deduplicated using Farce.dedup, then made Ractor-shareable. This may update and freeze the original. Already-shareable values pass through unchanged.
  • :shareable_copy - The value will be copied and the copy will be made Ractor-shareable.

Parameters:

  • initial_value (BasicObject, nil) —

    the value to store when the current value is nil

  • mode (Symbol, nil) (defaults to: nil) —

    the transfer mode, or nil to use the atom's default mode

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

    the maximum number of seconds to wait

Yields:

  • receives a non-nil current value and computes its replacement

Yield Parameters:

  • current (BasicObject) —

    the current value

Yield Returns:

  • (BasicObject, nil) —

    the replacement value

Returns:

  • (BasicObject, nil) —

    the current replacement or initial value, or nil when the timeout expires

Raises:

  • (LocalJumpError)


204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/farce/atom.rb', line 204

def upsert(initial_value, mode: nil, timeout: nil)
  raise LocalJumpError, "no block given" unless block_given?
  check_frozen!

  result = @atom.update(timeout:) do |current|
    if NIL_VALUE.equal?(current)
      wrap_value(initial_value, mode:)
    else
      value = yield(unwrap_value(current))
      check_frozen!
      wrap_value(value, mode:)
    end
  end
  unwrap_value(result)
end

#value ⇒ BasicObject?

Return the current value without waiting for an update in progress.

Returns:

  • (BasicObject, nil) —

    the current value



91
# File 'lib/farce/atom.rb', line 91

def value = unwrap_value(@atom.value)

#value=(new_value) ⇒ BasicObject?

Store a value using the default mode.

Parameters:

  • new_value (BasicObject, nil) —

    the new value

Returns:

  • (BasicObject, nil) —

    the new value



96
97
98
# File 'lib/farce/atom.rb', line 96

def value=(new_value)
  store(new_value)
end

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

Wait until the current value no longer matches an expected value.

Parameters:

  • expected (BasicObject, nil) —

    the value to compare with the current value

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

    the maximum number of seconds to wait

Yields:

  • called when the timeout expires

Returns:

  • (BasicObject, nil) —

    the changed value or the fallback result



225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/farce/atom.rb', line 225

def wait_until_changed(expected, timeout: nil, &)
  expected = wrap_comparison(expected)
  deadline = Internal.timeout_deadline(timeout)

  while true
    current = @atom.get(timeout: Internal.remaining_timeout(deadline)) { TIMED_OUT }
    return unwrap_result(current, &) if TIMED_OUT.equal?(current)
    return unwrap_value(current) unless values_equal?(current, expected)

    result = @atom.wait_until_changed(current, timeout: Internal.remaining_timeout(deadline)) { TIMED_OUT }
    return unwrap_result(result, &) if TIMED_OUT.equal?(result)
  end
end

#wait_until_non_nil(timeout: nil) { ... } ⇒ BasicObject?

Wait until the current value is not nil.

Parameters:

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

    the maximum number of seconds to wait

Yields:

  • called when the timeout expires

Returns:

  • (BasicObject, nil) —

    the non-nil value or the fallback result



243
# File 'lib/farce/atom.rb', line 243

def wait_until_non_nil(timeout: nil, &) = wait_until_changed(nil, timeout:, &)