farce 0.1.0-aarch64-linux-gnu → 0.2.0-aarch64-linux-gnu

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -1
  3. data/README.md +72 -16
  4. data/docs/gems/dry-types.md +8 -8
  5. data/docs/modes.md +67 -10
  6. data/ext/farce/atom.c +37 -22
  7. data/ext/farce/bounded_map.c +17 -13
  8. data/ext/farce/containers.h +15 -0
  9. data/ext/farce/counter.c +13 -18
  10. data/ext/farce/exchanger.c +5 -1
  11. data/ext/farce/farce.c +1 -1
  12. data/ext/farce/flag.c +39 -3
  13. data/ext/farce/io.c +3 -3
  14. data/ext/farce/lock.c +17 -23
  15. data/ext/farce/map.c +88 -66
  16. data/ext/farce/priority_queue.c +55 -43
  17. data/ext/farce/queue.c +10 -4
  18. data/ext/farce/reactor.c +52 -25
  19. data/ext/farce/reactor.h +1 -1
  20. data/ext/farce/signal.c +5 -1
  21. data/ext/farce/transaction.c +20 -24
  22. data/ext/farce/tree_map.c +33 -18
  23. data/ext/farce/trie.c +81 -43
  24. data/ext/farce/unshareable.c +168 -19
  25. data/ext/farce/unshared_io_pool.h +21 -12
  26. data/ext/farce/unshared_signal.c +13 -2
  27. data/ext/farce/unshared_wait.h +3 -0
  28. data/ext/farce/vector.c +82 -18
  29. data/ext/farce/weak_map.c +75 -63
  30. data/lib/farce/_yard/macros.rb +18 -3
  31. data/lib/farce/abstract/flag.rb +73 -0
  32. data/lib/farce/abstract/queue.rb +4 -3
  33. data/lib/farce/abstract/scheduler.rb +3 -3
  34. data/lib/farce/abstract/set.rb +17 -5
  35. data/lib/farce/abstract/sorted_set.rb +2 -2
  36. data/lib/farce/abstract/timer_queue.rb +49 -10
  37. data/lib/farce/abstract/vector.rb +510 -28
  38. data/lib/farce/atom.rb +1 -1
  39. data/lib/farce/class_mirror.rb +20 -7
  40. data/lib/farce/config.rb +7 -1
  41. data/lib/farce/engine/jvm/flag.rb +15 -3
  42. data/lib/farce/engine/ruby/3.4/farce.so +0 -0
  43. data/lib/farce/engine/ruby/4.0/farce.so +0 -0
  44. data/lib/farce/engine/ruby/shared/require_hook.rb +18 -0
  45. data/lib/farce/engine/ruby.rb +7 -0
  46. data/lib/farce/engine/shared/unshared_vector.rb +31 -4
  47. data/lib/farce/engine/shared/vector.rb +8 -0
  48. data/lib/farce/engine/shared/weak_map/index.rb +14 -1
  49. data/lib/farce/engine/truffleruby/native/flag.rb +15 -3
  50. data/lib/farce/envelope.rb +54 -7
  51. data/lib/farce/exchanger.rb +1 -1
  52. data/lib/farce/flag.rb +2 -0
  53. data/lib/farce/integrations/active_support.rb +1 -1
  54. data/lib/farce/integrations/bson.rb +1 -1
  55. data/lib/farce/integrations/cbor.rb +1 -1
  56. data/lib/farce/integrations/concurrent.rb +3 -3
  57. data/lib/farce/integrations/date.rb +8 -0
  58. data/lib/farce/integrations/dry_types.rb +7 -4
  59. data/lib/farce/integrations/json.rb +2 -2
  60. data/lib/farce/integrations/msgpack.rb +1 -1
  61. data/lib/farce/integrations/oj.rb +3 -4
  62. data/lib/farce/integrations/psych.rb +1 -1
  63. data/lib/farce/integrations/ractor_sharing.rb +1 -1
  64. data/lib/farce/integrations/sorted_set.rb +1 -1
  65. data/lib/farce/integrations/weakref.rb +9 -1
  66. data/lib/farce/integrations/yajl.rb +3 -3
  67. data/lib/farce/integrations.rb +33 -23
  68. data/lib/farce/internal/flag_waiting.rb +30 -0
  69. data/lib/farce/internal/freeze.rb +8 -3
  70. data/lib/farce/internal/map_value_modes.rb +1 -1
  71. data/lib/farce/internal/scheduler_lifecycle.rb +1 -1
  72. data/lib/farce/lazy.rb +4 -4
  73. data/lib/farce/lazy_ref.rb +3 -3
  74. data/lib/farce/lfu_map.rb +1 -1
  75. data/lib/farce/local/flag.rb +4 -0
  76. data/lib/farce/local/scoped.rb +1 -1
  77. data/lib/farce/lru_map.rb +1 -1
  78. data/lib/farce/map.rb +2 -2
  79. data/lib/farce/mode_manager/smart_rule.rb +138 -0
  80. data/lib/farce/mode_manager.rb +12 -3
  81. data/lib/farce/molecule.rb +2 -2
  82. data/lib/farce/pool.rb +1 -1
  83. data/lib/farce/port.rb +4 -4
  84. data/lib/farce/priority_queue.rb +1 -1
  85. data/lib/farce/proxy/register.rb +6 -0
  86. data/lib/farce/proxy/supervisor.rb +1 -1
  87. data/lib/farce/proxy.rb +7 -2
  88. data/lib/farce/queue.rb +1 -1
  89. data/lib/farce/scheduler.rb +2 -2
  90. data/lib/farce/set.rb +1 -1
  91. data/lib/farce/sorted_set.rb +1 -1
  92. data/lib/farce/timer_queue.rb +20 -6
  93. data/lib/farce/transaction/vector.rb +43 -9
  94. data/lib/farce/transaction/wrapper.rb +14 -0
  95. data/lib/farce/tree_map.rb +1 -1
  96. data/lib/farce/unsafe/lfu_map.rb +2 -0
  97. data/lib/farce/unsafe/lru_map.rb +2 -0
  98. data/lib/farce/unsafe/tree_map.rb +2 -0
  99. data/lib/farce/unshareable.rb +8 -19
  100. data/lib/farce/unshared/atom.rb +3 -0
  101. data/lib/farce/unshared/lfu_map.rb +2 -0
  102. data/lib/farce/unshared/lru_map.rb +2 -0
  103. data/lib/farce/unshared/map.rb +3 -0
  104. data/lib/farce/unshared/set.rb +1 -0
  105. data/lib/farce/unshared/sorted_set.rb +1 -0
  106. data/lib/farce/unshared/tree_map.rb +5 -1
  107. data/lib/farce/unshared/vector.rb +3 -0
  108. data/lib/farce/unshared/weak_atom.rb +5 -1
  109. data/lib/farce/unshared/weak_key_map.rb +3 -0
  110. data/lib/farce/unshared/weak_map.rb +3 -0
  111. data/lib/farce/unshared/weak_set.rb +1 -0
  112. data/lib/farce/unshared/weak_value_map.rb +3 -0
  113. data/lib/farce/vector.rb +44 -19
  114. data/lib/farce/version.rb +1 -1
  115. data/lib/farce.rb +11 -8
  116. metadata +5 -1
@@ -10,6 +10,7 @@ module Farce
10
10
  active_support: %w[active_support active_support/core_ext].freeze,
11
11
  bson: %w[bson].freeze,
12
12
  cbor: %w[cbor].freeze,
13
+ date: %w[date].freeze,
13
14
  dry_types: %w[dry/types].freeze,
14
15
  concurrent: %w[concurrent/map concurrent/tvar concurrent].freeze,
15
16
  json: %w[json].freeze,
@@ -61,28 +62,6 @@ module Farce
61
62
  end
62
63
  private_constant :RequireHook
63
64
 
64
- # A prepended singleton hook can recurse when Zeitwerk aliases it. Preserve
65
- # the original method explicitly instead of relying on super here.
66
- class << Kernel
67
- alias farce_original_require require
68
- private :farce_original_require
69
-
70
- # Load a feature and its integration through Kernel.require.
71
- # @api private
72
- # @param path [String, #to_path] The feature to load.
73
- # @return [Boolean] Whether the requested feature was newly loaded.
74
- def require(path, ...)
75
- path = File.path(path)
76
- result = Integrations.loading(path) { farce_original_require(path, ...) }
77
- integration = PATHS[path.delete_suffix(".rb")]
78
- if integration && !Integrations.loading?(integration) && Farce.config.autoload_integrations
79
- integration_path = "farce/integrations/#{integration}"
80
- Integrations.loading(integration_path) { farce_original_require(integration_path) }
81
- end
82
- result
83
- end
84
- end
85
-
86
65
  private_constant :INTEGRATIONS, :PATHS
87
66
 
88
67
  extend self
@@ -106,8 +85,39 @@ module Farce
106
85
  # Activate integrations at startup when automatic loading is enabled.
107
86
  # @api private
108
87
  def setup
88
+ return unless Farce.config.autoload_integrations
89
+ install_require_hooks
90
+ load_active
91
+ end
92
+
93
+ # Install hooks once, including when automatic loading is enabled later.
94
+ # @api private
95
+ def install_require_hooks
96
+ return if Object.ancestors.include?(RequireHook)
97
+ # The instance hook handles ordinary require calls. The singleton alias
98
+ # chain handles Kernel.require, which bypasses Object's instance methods.
109
99
  Object.prepend(RequireHook)
110
- load_active if Farce.config.autoload_integrations
100
+ # A prepended singleton hook can recurse when Zeitwerk aliases it. Preserve
101
+ # the original method explicitly instead of relying on super here.
102
+ class << Kernel
103
+ alias farce_original_require require
104
+ private :farce_original_require
105
+
106
+ # Load a feature and its integration through Kernel.require.
107
+ # @api private
108
+ # @param path [String, #to_path] The feature to load.
109
+ # @return [Boolean] Whether the requested feature was newly loaded.
110
+ def require(path, ...)
111
+ path = File.path(path)
112
+ result = Integrations.loading(path) { farce_original_require(path, ...) }
113
+ integration = PATHS[path.delete_suffix(".rb")]
114
+ if integration && !Integrations.loading?(integration) && Farce.config.autoload_integrations
115
+ integration_path = "farce/integrations/#{integration}"
116
+ Integrations.loading(integration_path) { farce_original_require(integration_path) }
117
+ end
118
+ result
119
+ end
120
+ end
111
121
  end
112
122
 
113
123
  # Loads all integrations for gems that have already been loaded.
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ # @!visibility private
7
+ module Internal # :nodoc: all
8
+ module FlagWaiting
9
+ # Wait until the current value differs from the expected value.
10
+ # @param expected [Boolean] the value to wait to change from
11
+ # @param timeout [Numeric, nil] the total seconds available
12
+ # @yield called when the timeout expires
13
+ # @return [Boolean, BasicObject, nil] the changed value or timeout fallback
14
+ # @raise [ArgumentError] if expected is not true or false
15
+ def wait_until_changed(expected, timeout: nil)
16
+ unless expected.equal?(true) || expected.equal?(false)
17
+ raise ArgumentError, "expected value must be true or false"
18
+ end
19
+ signal = change_signal
20
+ Internal.with_timeout(timeout) do |_, deadline|
21
+ observed = signal.generation
22
+ current = value
23
+ return current unless expected == current
24
+ break unless signal.wait(observed, timeout: Internal.remaining_timeout(deadline))
25
+ end
26
+ yield if block_given?
27
+ end
28
+ end
29
+ end
30
+ end
@@ -4,7 +4,7 @@
4
4
 
5
5
  module Farce
6
6
  module Internal
7
- # Shared implementation for the explicit frozen-state policies in {Shareable}.
7
+ # Frozen-state policies for wrappers with separate backing storage.
8
8
  module Freeze
9
9
  # Structurally freeze a Ruby facade, then validate and publish its object graph.
10
10
  # This deliberately bypasses an override that controls logical frozen state.
@@ -20,14 +20,14 @@ module Farce
20
20
  raise FrozenError.new("can't modify frozen #{object.class}", receiver: object)
21
21
  end
22
22
 
23
- # Delegate logical frozen state to storage owned by the wrapper.
23
+ # Freeze owned storage and the facade without freezing stored values.
24
24
  module Delegated
25
25
  def freeze
26
26
  backend = freeze_backend
27
27
  raise "#{self.class} is not initialized" unless backend
28
28
 
29
29
  backend.freeze
30
- self
30
+ Object.instance_method(:freeze).bind_call(self)
31
31
  end
32
32
 
33
33
  def frozen?
@@ -39,6 +39,11 @@ module Farce
39
39
 
40
40
  def check_frozen! = Internal::Freeze.check(self)
41
41
 
42
+ def initialize_clone(other, freeze: nil)
43
+ super
44
+ self.freeze if is_a?(Farce::Unshareable) && (freeze.nil? ? other.frozen? : freeze)
45
+ end
46
+
42
47
  def publish_shareable_copy(other, operation:, freeze: nil)
43
48
  logically_frozen = operation == :clone && (freeze.nil? ? other.frozen? : freeze)
44
49
  self.freeze if logically_frozen
@@ -18,7 +18,7 @@ module Farce
18
18
  # @param compare_by_identity [Boolean] the default comparison mode for keys and values
19
19
  # @param compare_keys_by_identity [Boolean] whether keys are compared by identity
20
20
  # @param compare_values_by_identity [Boolean] whether values are compared by identity
21
- def initialize(initial_mapping = nil, mode: :copy, normalize_keys: nil, compare_by_identity: false,
21
+ def initialize(initial_mapping = nil, mode: :smart, normalize_keys: nil, compare_by_identity: false,
22
22
  compare_keys_by_identity: compare_by_identity, compare_values_by_identity: compare_by_identity)
23
23
  initial_mapping = convert_entries(initial_mapping)
24
24
 
@@ -15,7 +15,7 @@ module Farce
15
15
  module SchedulerLifecycle
16
16
  include Farce::Unshareable
17
17
 
18
- # Prepending bypasses Unshareable's include hook, so remove freeze here.
18
+ # A live scheduler must keep updating its own lifecycle state.
19
19
  def self.prepended(scheduler)
20
20
  scheduler.undef_method(:freeze)
21
21
  end
data/lib/farce/lazy.rb CHANGED
@@ -5,7 +5,7 @@
5
5
  module Farce
6
6
  # A ractor-shareable lazy value that is computed on demand.
7
7
  # The factory runs once. Its result is transferred according to {#mode}.
8
- # With the default `:copy` mode, each Ractor receives its own cached copy.
8
+ # The default `:smart` mode prepares results using smart rules or dedicated proxies.
9
9
  # The factory and its bound receiver must remain Ractor-shareable.
10
10
  # Use {Strict::Lazy} for direct shareable results or {Unshared::Lazy} for local factories.
11
11
  #
@@ -63,15 +63,15 @@ module Farce
63
63
  include Shareable::Tracked
64
64
 
65
65
  # @!macro modes
66
- # @overload initialize(factory, mode: :copy)
66
+ # @overload initialize(factory, mode: :smart)
67
67
  # @param factory [Class, Proc, #call] the shareable factory for the value
68
68
  # @param mode [Symbol] the transfer mode for the computed result
69
- # @overload initialize(mode: :copy, self: nil)
69
+ # @overload initialize(mode: :smart, self: nil)
70
70
  # @param mode [Symbol] the transfer mode for the computed result
71
71
  # @param self [BasicObject] the shareable receiver to bind to the block
72
72
  # @yield computes the value on first access
73
73
  # @yieldreturn [BasicObject] the result to transfer
74
- def initialize(factory = nil, mode: :copy, **, &)
74
+ def initialize(factory = nil, mode: :smart, **, &)
75
75
  @manager = marshal_mode_manager(mode)
76
76
  super(factory, **, &)
77
77
  end
@@ -6,7 +6,7 @@ module Farce
6
6
  # @!parse
7
7
  # # A {Reference} for a {Lazy} value.
8
8
  # #
9
- # # Results use the lazy value's transfer mode, which defaults to `:copy`.
9
+ # # Results use the lazy value's transfer mode, which defaults to `:smart`.
10
10
  # #
11
11
  # # Calling `freeze` resolves and freezes the target. Checking `frozen?` before
12
12
  # # the first successful freeze does not resolve it.
@@ -24,11 +24,11 @@ module Farce
24
24
  # # initialized.value # => 1
25
25
  # class LazyRef < Farce::Reference
26
26
  # # @!macro modes
27
- # # @overload initialize(factory, mode: :copy)
27
+ # # @overload initialize(factory, mode: :smart)
28
28
  # # @param mode [Symbol] the transfer mode for the computed result
29
29
  # # @param [Class, Proc, #call] factory The factory to use for creating the value. Must be ractor-shareable.
30
30
  # #
31
- # # @overload initialize(self: nil, mode: :copy)
31
+ # # @overload initialize(self: nil, mode: :smart)
32
32
  # # @param mode [Symbol] the transfer mode for the computed result
33
33
  # # @yield The block to use for creating the value. Must be ractor-shareable.
34
34
  # # @yieldreceiver [BasicObject] The `self` parameter provided, or `nil` if not provided.
data/lib/farce/lfu_map.rb CHANGED
@@ -13,7 +13,7 @@ module Farce
13
13
  # @param entries [Hash, Array<Array(BasicObject, BasicObject)>, Abstract::Map, #each, nil]
14
14
  # Optional initial entries. Entries are stored sequentially and may be evicted.
15
15
  # @param mode [Symbol] The default value transfer mode.
16
- def initialize(entries = nil, mode: :copy, **)
16
+ def initialize(entries = nil, mode: :smart, **)
17
17
  @manager = marshal_mode_manager(mode)
18
18
  super(entries, **)
19
19
  end
@@ -35,6 +35,9 @@ module Farce
35
35
  # @return [Boolean] the new value
36
36
  def store(value) = scoped_value.store(value)
37
37
 
38
+ # (see Abstract::Flag#wait_until_changed)
39
+ def wait_until_changed(...) = scoped_value.wait_until_changed(...)
40
+
38
41
  alias value= store
39
42
 
40
43
  # Replace the value and return its previous state.
@@ -60,6 +63,7 @@ module Farce
60
63
  end
61
64
 
62
65
  def new_scoped_value(...) = Internal::Flag.new(...)
66
+ def change_signal = scoped_value.change_signal
63
67
  end
64
68
  end
65
69
  end
@@ -74,7 +74,7 @@ module Farce
74
74
  def new_copied_scoped_value(map) = map
75
75
  end
76
76
 
77
- MANAGER = ModeManager.new
77
+ MANAGER = ModeManager.new(mode: :copy)
78
78
  private_constant :MANAGER, :Map
79
79
 
80
80
  # @!visibility private
data/lib/farce/lru_map.rb CHANGED
@@ -23,7 +23,7 @@ module Farce
23
23
  # @param entries [Hash, Array<Array(BasicObject, BasicObject)>, Abstract::Map, #each, nil]
24
24
  # Optional initial entries. Entries are stored sequentially and may be evicted.
25
25
  # @param mode [Symbol] The default value transfer mode.
26
- def initialize(entries = nil, mode: :copy, **)
26
+ def initialize(entries = nil, mode: :smart, **)
27
27
  @manager = marshal_mode_manager(mode)
28
28
  super(entries, **)
29
29
  end
data/lib/farce/map.rb CHANGED
@@ -30,11 +30,11 @@ module Farce
30
30
  # class Map
31
31
  # # @!macro map_initialization
32
32
  # # @!macro modes
33
- # # @param mode [Symbol] The default transfer mode for values. Defaults to `:copy`.
33
+ # # @param mode [Symbol] The default transfer mode for values. Defaults to `:smart`.
34
34
  # # @return [Map]
35
35
  # def initialize(
36
36
  # initial_mapping = nil,
37
- # mode: :copy,
37
+ # mode: :smart,
38
38
  # normalize_keys: nil,
39
39
  # compare_by_identity: false,
40
40
  # compare_keys_by_identity: compare_by_identity,
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ class ModeManager
7
+ # Class for converting values based on smart rules.
8
+ class SmartRule
9
+ SMART_MODES = %i[smart smart_raise smart_move smart_copy].freeze
10
+ private_constant :SMART_MODES
11
+
12
+ include Shareable::Immutable
13
+
14
+ # @note This method must be called from the main ractor.
15
+ # Allows you to add rules for smart wrapping of specific classes.
16
+ #
17
+ # @overload define(klass, mode = nil)
18
+ # @example
19
+ # # Have smart modes use the :make_shareable mode for instances of MyClass
20
+ # Farce::ModeManager::SmartRule.define(MyClass, :make_shareable)
21
+ #
22
+ # @param klass [Class] the class to define a smart wrapping rule for
23
+ # @param mode [Symbol] the mode to use when wrapping instances of the class
24
+ #
25
+ # @overload define(klass)
26
+ # @example
27
+ # # Have smart modes use a custom block for instances of MyClass
28
+ # Farce::ModeManager::SmartRule.define(MyClass) do |value, mode_manager|
29
+ # raise Ractor::Error, "#{value.inspect} cannot be shared" if value.dont_share?
30
+ # mode_manager.wrap(value, mode: :make_shareable)
31
+ # end
32
+ #
33
+ # @param klass [Class] the class to define a smart wrapping rule for
34
+ # @yield [value, mode_manager, mode] The block will be called when wrapping an instance of klass.
35
+ # @yieldparam value [BasicObject] The value being wrapped.
36
+ # @yieldparam mode_manager [Farce::ModeManager] The mode manager instance.
37
+ # @yieldparam mode [Symbol] The mode to use when wrapping the value, like `:smart` or `:smart_copy`.
38
+ # @yieldreturn [BasicObject] The shareable version of the value.
39
+ #
40
+ # @overload define(klass)
41
+ # @example
42
+ # Farce::ModeManager::SmartRule.define(MyClass) do
43
+ # def smart_copy(value, mode_manager) = mode_manager.wrap(value, mode: :copy)
44
+ # def smart_move(value, mode_manager) = mode_manager.wrap(value, mode: :move)
45
+ # end
46
+ #
47
+ # @yield block used for defining different methods for different smart modes.
48
+ # @yieldreceiver [Class] The {SmartRule} subclass.
49
+ #
50
+ # @return [Class] the defined smart rule class.
51
+ def self.define(klass, mode = nil, &block)
52
+ SMART_RULES.define(klass) do
53
+ if mode
54
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
55
+ def call(value, mode_manager, _) = mode_manager.wrap(value, mode: #{mode.inspect})
56
+ RUBY
57
+ end
58
+
59
+ if block_given?
60
+ if block.arity.zero?
61
+ class_eval(&block)
62
+ SMART_MODES.each { private it if method_defined?(it) }
63
+ else
64
+ define_method(:call, &Ractor.shareable_lambda(&block))
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # @api private
71
+ def call(value, mode_manager, mode)
72
+ register = mode_manager.register || Proxy.default_register
73
+ return Proxy.new(value, register:) if register.dedicated_proxy?(value.class)
74
+ __send__(mode, value, mode_manager)
75
+ rescue TypeError => e
76
+ raise unless e.message.include?("allocator undefined for")
77
+ raise Ractor::IsolationError, "#{value.inspect} cannot be shared"
78
+ end
79
+
80
+ # @api private
81
+ def marshal_dump = raise TypeError, "smart rules cannot be marshaled"
82
+
83
+ private
84
+
85
+ # smart is intentionally not an alias
86
+ def smart(value, mode_manager) = smart_raise(value, mode_manager)
87
+ def smart_raise(value, _) = raise Ractor::IsolationError, "#{value.inspect} cannot be shared"
88
+ def smart_move(value, mode_manager) = mode_manager.wrap(value, mode: :move)
89
+
90
+ def smart_copy(value, mode_manager)
91
+ mode_manager.wrap(value, mode: :copy)
92
+ rescue StandardError => e
93
+ begin
94
+ mode_manager.wrap(value, mode: :marshal)
95
+ rescue StandardError
96
+ raise e, cause: e.cause
97
+ end
98
+ end
99
+ end
100
+
101
+ SMART_RULES = ClassMirror.new(SmartRule) { |klass, _| klass.new }
102
+ private_constant :SMART_RULES
103
+
104
+ SmartRule.define(Time, :make_shareable)
105
+
106
+ [Array, ::Set].each do |klass|
107
+ SmartRule.define(klass) do
108
+ def smart_copy(value, mode_manager)
109
+ all_shareable = true
110
+ mapped = value.dup.map! do |entry|
111
+ result = mode_manager.wrap(entry, mode: :smart_raise)
112
+ if mode_manager.managed_envelope?(result)
113
+ all_shareable = false
114
+ entry
115
+ else
116
+ result
117
+ end
118
+ rescue StandardError
119
+ all_shareable = false
120
+ entry
121
+ end
122
+ return super unless all_shareable
123
+ Ractor.make_shareable(mapped)
124
+ end
125
+
126
+ def smart(value, mode_manager)
127
+ return super unless value.all? { Ractor.shareable?(it) }
128
+ Ractor.make_shareable(value)
129
+ end
130
+ end
131
+ end
132
+
133
+ SmartRule.define(Hash) do |value, mode_manager, mode|
134
+ return super(value, mode_manager, mode) unless value.all? { _1.is_a?(Symbol) && Ractor.shareable?(_2) }
135
+ Ractor.make_shareable(value)
136
+ end
137
+ end
138
+ end
@@ -2,6 +2,8 @@
2
2
  # shareable_constant_value: literal
3
3
  # warn_indent: true
4
4
 
5
+ require "farce/mode_manager/smart_rule"
6
+
5
7
  module Farce
6
8
  # A mode manager encapsulates logic for dealing with Ruby objects that aren't Ractor-shareable.
7
9
  # It is a perfect companion piece for APIs that require Ractor-shareable objects.
@@ -17,7 +19,7 @@ module Farce
17
19
  # class MyQueue
18
20
  # include Farce::Shareable
19
21
  #
20
- # def initialize(capacity: 1024, mode: :copy)
22
+ # def initialize(capacity: 1024, mode: :smart)
21
23
  # @queue = Ratomic::Queue.new(capacity)
22
24
  # @manager = Farce::ModeManager.new(mode:)
23
25
  # end
@@ -55,10 +57,13 @@ module Farce
55
57
  # @return [Symbol] The default mode to use when wrapping values.
56
58
  attr_reader :mode
57
59
 
60
+ # @return [Farce::Proxy::Register, nil] The register used for generating Farce::Proxy instances.
61
+ attr_reader :register
62
+
58
63
  # @!macro modes
59
64
  # @param mode [Symbol] The default mode to use when wrapping values. Must be one of the valid modes.
60
65
  # @param register [Proxy::Register, nil] The register to use for generating Farce::Proxy instances.
61
- def initialize(mode: :copy, register: nil)
66
+ def initialize(mode: :smart, register: nil)
62
67
  raise ArgumentError, "invalid mode: #{mode.inspect}" unless Farce::MODES.include?(mode)
63
68
  @mode = mode
64
69
  @register = register
@@ -87,7 +92,10 @@ module Farce
87
92
  def wrap(value, mode: nil)
88
93
  return value if Ractor.shareable?(value)
89
94
 
90
- case mode || self.mode
95
+ case mode ||= self.mode
96
+ when :smart, :smart_copy, :smart_move, :smart_raise
97
+ return Ractor.make_shareable(-value) if value.is_a?(String)
98
+ SMART_RULES[value.class].call(value, self, mode)
91
99
  when :copy then Envelope::Copy.new(value, self)
92
100
  when :dedup then Ractor.make_shareable(Farce.dedup(value))
93
101
  when :local then Envelope::Local.new(value, self)
@@ -97,6 +105,7 @@ module Farce
97
105
  when :raise then raise Ractor::IsolationError, "value is not Ractor-shareable: #{value.inspect}"
98
106
  when :shareable_copy then Ractor.make_shareable(value, copy: true)
99
107
  when :proxy then Proxy.new(value, register: @register)
108
+ when :marshal then Envelope::Marshal.new(value, self)
100
109
  else raise ArgumentError, "invalid mode: #{mode.inspect}"
101
110
  end
102
111
  end
@@ -21,7 +21,7 @@ module Farce
21
21
  # Create a record class with defaults for newly created atoms.
22
22
  # @!macro modes
23
23
  # @param fields [Array<Symbol, String>] the field names
24
- # @param mode [Symbol, nil] the default transfer mode, or nil for :copy
24
+ # @param mode [Symbol, nil] the default transfer mode, or nil for :smart
25
25
  # @param compare_by_identity [Boolean, nil] the default comparison policy
26
26
  # @return [Class<Molecule>]
27
27
  def self.define(*fields, mode: nil, **, &)
@@ -32,7 +32,7 @@ module Farce
32
32
  end
33
33
 
34
34
  # @return [Symbol] the default transfer mode for new records
35
- def self.default_mode = :copy
35
+ def self.default_mode = :smart
36
36
 
37
37
  # @!visibility private
38
38
  def self.define_member(field)
data/lib/farce/pool.rb CHANGED
@@ -111,7 +111,7 @@ module Farce
111
111
  # @yield [*args] Task to execute.
112
112
  # @return [Pool] self.
113
113
  # @raise [PoolClosedError] If the pool is closing or closed.
114
- def schedule(*args, mode: :copy, auto_local: true, &block) # rubocop:disable Lint/UnusedMethodArgument
114
+ def schedule(*args, mode: :smart, auto_local: true, &block) # rubocop:disable Lint/UnusedMethodArgument
115
115
  raise PoolClosedError, "cannot schedule task on a closed pool" if closed?
116
116
  raise ArgumentError, "local transfer is not supported by a pool" if mode == :local
117
117
 
data/lib/farce/port.rb CHANGED
@@ -12,9 +12,9 @@ module Farce
12
12
  class Port < Internal::Port
13
13
  include Abstract::Port
14
14
 
15
- MANAGER = ModeManager.new
15
+ MANAGER = ModeManager.new(mode: :smart_copy)
16
16
  SUBCLASSES = Farce::MODES.to_h do |mode|
17
- if mode == :copy
17
+ if mode == :smart_copy
18
18
  normal = self
19
19
  else
20
20
  normal = Class.new(self)
@@ -41,7 +41,7 @@ module Farce
41
41
  end
42
42
 
43
43
  # @api private
44
- def self.mode = :copy
44
+ def self.mode = :smart_copy
45
45
 
46
46
  # @api private
47
47
  def self.auto_local? = false
@@ -54,7 +54,7 @@ module Farce
54
54
  self[mode || self.mode, auto_local: auto_local].new
55
55
  end
56
56
 
57
- # @!method initialize(mode: :copy, auto_local: false)
57
+ # @!method initialize(mode: :smart_copy, auto_local: false)
58
58
  # Creates a new port with the given default mode.
59
59
  #
60
60
  # @!macro modes
@@ -11,7 +11,7 @@ module Farce
11
11
  # (see Farce::Abstract::PriorityQueue#initialize)
12
12
  # @!macro modes
13
13
  # @param mode [Symbol] the default mode for sending unshareable values
14
- def initialize(capacity: nil, default_priority: 0, order: :ascending, mode: :copy, track_age: false)
14
+ def initialize(capacity: nil, default_priority: 0, order: :ascending, mode: :smart, track_age: false)
15
15
  @manager = ModeManager.new(mode:)
16
16
  super(capacity:, default_priority:, order:, track_age:)
17
17
  end
@@ -87,6 +87,12 @@ module Farce
87
87
  end
88
88
  end
89
89
 
90
+ # Check whether this exact class has a definition in either proxy layer.
91
+ # Inherited wrapper methods do not count as a dedicated proxy.
92
+ # @param klass [Class] the source class
93
+ # @return [Boolean] whether the class has a dedicated proxy
94
+ def dedicated_proxy?(klass) = @inner_mirror.custom?(klass) || @outer_mirror.custom?(klass)
95
+
90
96
  private
91
97
 
92
98
  def mirror_for(layer)
@@ -120,7 +120,7 @@ module Farce
120
120
  rescue Exception => e # rubocop:disable Lint/RescueException
121
121
  request.success.value = false
122
122
  begin
123
- result = @mode_manager.wrap(e)
123
+ result = @mode_manager.wrap(e, mode: :copy)
124
124
  rescue StandardError
125
125
  result = -"#{e.class}: #{e.message}"
126
126
  end
data/lib/farce/proxy.rb CHANGED
@@ -50,13 +50,18 @@ module Farce
50
50
  # ```
51
51
  #
52
52
  # The above example has one issue:
53
- # We cannot chain the calls, because `<<` returns an array, which actually gets copied.
53
+ # By default, `<<` makes the returned array shareable and freezes it.
54
+ # The next mutation raises `FrozenError`.
54
55
  #
55
56
  # ```ruby
56
57
  # my_instance = MyClass.new
57
58
  # my_proxy = Farce::Proxy.create(my_instance)
58
59
  #
59
- # Ractor.new(my_proxy) { it << 42 << 43 }.join
60
+ # Ractor.new(my_proxy) do |proxy|
61
+ # proxy << 42 << 43
62
+ # rescue FrozenError
63
+ # :frozen_result
64
+ # end.join
60
65
  #
61
66
  # my_instance.record # => [42]
62
67
  # ```
data/lib/farce/queue.rb CHANGED
@@ -11,7 +11,7 @@ module Farce
11
11
  # (see Farce::Abstract::Queue#initialize)
12
12
  # @!macro modes
13
13
  # @param mode [Symbol] the default mode for sending unshareable values
14
- def initialize(capacity: 1024, mode: :copy, track_age: false)
14
+ def initialize(capacity: 1024, mode: :smart, track_age: false)
15
15
  @manager = ModeManager.new(mode:)
16
16
  @queue = Internal::Queue.new(capacity:, track_age:)
17
17
  super()
@@ -326,14 +326,14 @@ module Farce
326
326
  #
327
327
  # @param args [Array<Object>] Positional arguments passed to the task block.
328
328
  # @param mode [Symbol]
329
- # Argument transfer mode: `:copy`, `:move`, `:local`, `:make_shareable`, `:shareable_copy`, or `:raise`.
329
+ # Argument transfer mode from {Farce::MODES}, including `:copy`, `:marshal`, and `:local`.
330
330
  # See {ModeManager#wrap}.
331
331
  # @param auto_local [Boolean] Whether to override mode with `:local` in the owning Ractor.
332
332
  # @yield [*args] The task to execute.
333
333
  # @return [Scheduler] self.
334
334
  # @raise [SchedulerClosedError] If the handle is closing, closed, or in an error state.
335
335
  # @see Ractor.shareable_proc
336
- def schedule(*args, mode: :copy, auto_local: true, &block)
336
+ def schedule(*args, mode: :smart, auto_local: true, &block)
337
337
  raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if closed?
338
338
 
339
339
  if auto_local
data/lib/farce/set.rb CHANGED
@@ -30,7 +30,7 @@ module Farce
30
30
  private
31
31
 
32
32
  def initialize_value_mode(mode)
33
- mode = :copy if UNDEFINED.equal?(mode)
33
+ mode = :smart if UNDEFINED.equal?(mode)
34
34
  @manager = ModeManager.new(mode:)
35
35
  end
36
36
 
@@ -24,7 +24,7 @@ module Farce
24
24
  private
25
25
 
26
26
  def initialize_value_mode(mode)
27
- mode = :copy if UNDEFINED.equal?(mode)
27
+ mode = :smart if UNDEFINED.equal?(mode)
28
28
  @manager = ModeManager.new(mode:)
29
29
  end
30
30