farce 0.1.0-java → 0.3.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -1
  3. data/README.md +111 -17
  4. data/docs/gems/dry-types.md +8 -8
  5. data/docs/modes.md +75 -15
  6. data/docs/scopes.md +3 -1
  7. data/docs/variants.md +18 -0
  8. data/lib/farce/_yard/macros.rb +18 -3
  9. data/lib/farce/abstract/executor.rb +130 -0
  10. data/lib/farce/abstract/flag.rb +73 -0
  11. data/lib/farce/abstract/pool.rb +334 -0
  12. data/lib/farce/abstract/queue.rb +4 -3
  13. data/lib/farce/abstract/scheduler.rb +539 -93
  14. data/lib/farce/abstract/set.rb +17 -5
  15. data/lib/farce/abstract/sorted_set.rb +2 -2
  16. data/lib/farce/abstract/timer_queue.rb +49 -10
  17. data/lib/farce/abstract/vector.rb +510 -28
  18. data/lib/farce/atom.rb +1 -1
  19. data/lib/farce/class_mirror.rb +20 -7
  20. data/lib/farce/config.rb +7 -1
  21. data/lib/farce/engine/jruby/signal.rb +7 -4
  22. data/lib/farce/engine/jvm/flag.rb +15 -3
  23. data/lib/farce/engine/ruby/shared/require_hook.rb +18 -0
  24. data/lib/farce/engine/ruby.rb +7 -0
  25. data/lib/farce/engine/shared/main_scheduler.rb +2 -2
  26. data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
  27. data/lib/farce/engine/shared/proxy_owner.rb +1 -1
  28. data/lib/farce/engine/shared/unshared_atom.rb +9 -8
  29. data/lib/farce/engine/shared/unshared_vector.rb +31 -4
  30. data/lib/farce/engine/shared/vector.rb +8 -0
  31. data/lib/farce/engine/shared/weak_map/index.rb +14 -1
  32. data/lib/farce/engine/shared.rb +9 -0
  33. data/lib/farce/engine/truffleruby/native/flag.rb +15 -3
  34. data/lib/farce/envelope.rb +54 -7
  35. data/lib/farce/exchanger.rb +1 -1
  36. data/lib/farce/flag.rb +2 -0
  37. data/lib/farce/integrations/active_support.rb +1 -1
  38. data/lib/farce/integrations/bson.rb +1 -1
  39. data/lib/farce/integrations/cbor.rb +1 -1
  40. data/lib/farce/integrations/concurrent.rb +3 -3
  41. data/lib/farce/integrations/date.rb +8 -0
  42. data/lib/farce/integrations/dry_types.rb +7 -4
  43. data/lib/farce/integrations/json.rb +2 -2
  44. data/lib/farce/integrations/msgpack.rb +1 -1
  45. data/lib/farce/integrations/oj.rb +3 -4
  46. data/lib/farce/integrations/psych.rb +1 -1
  47. data/lib/farce/integrations/ractor_sharing.rb +1 -1
  48. data/lib/farce/integrations/sorted_set.rb +1 -1
  49. data/lib/farce/integrations/weakref.rb +9 -1
  50. data/lib/farce/integrations/yajl.rb +3 -3
  51. data/lib/farce/integrations.rb +33 -23
  52. data/lib/farce/internal/flag_waiting.rb +30 -0
  53. data/lib/farce/internal/freeze.rb +8 -3
  54. data/lib/farce/internal/lease_initialization.rb +1 -1
  55. data/lib/farce/internal/local_executor_methods.rb +33 -0
  56. data/lib/farce/internal/local_scheduled_task.rb +27 -0
  57. data/lib/farce/internal/map_value_modes.rb +1 -1
  58. data/lib/farce/internal/scheduled_task.rb +1 -1
  59. data/lib/farce/internal/scheduler_lifecycle.rb +1 -1
  60. data/lib/farce/internal/thread_pool_supervisor.rb +45 -0
  61. data/lib/farce/internal/unshared_pool_worker.rb +14 -0
  62. data/lib/farce/internal.rb +14 -0
  63. data/lib/farce/lazy.rb +4 -4
  64. data/lib/farce/lazy_ref.rb +3 -3
  65. data/lib/farce/lfu_map.rb +1 -1
  66. data/lib/farce/local/flag.rb +4 -0
  67. data/lib/farce/local/pool.rb +111 -0
  68. data/lib/farce/local/scheduler.rb +219 -0
  69. data/lib/farce/local/scoped.rb +1 -1
  70. data/lib/farce/lru_map.rb +1 -1
  71. data/lib/farce/map.rb +2 -2
  72. data/lib/farce/mode_manager/smart_rule.rb +138 -0
  73. data/lib/farce/mode_manager.rb +12 -3
  74. data/lib/farce/molecule.rb +2 -2
  75. data/lib/farce/pool.rb +2 -290
  76. data/lib/farce/port.rb +4 -4
  77. data/lib/farce/priority_queue.rb +1 -1
  78. data/lib/farce/proxy/register.rb +6 -0
  79. data/lib/farce/proxy/supervisor.rb +1 -1
  80. data/lib/farce/proxy.rb +9 -4
  81. data/lib/farce/queue.rb +1 -1
  82. data/lib/farce/scheduler.rb +1 -502
  83. data/lib/farce/set.rb +1 -1
  84. data/lib/farce/sorted_set.rb +1 -1
  85. data/lib/farce/task.rb +285 -0
  86. data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
  87. data/lib/farce/timer_queue.rb +20 -6
  88. data/lib/farce/transaction/vector.rb +43 -9
  89. data/lib/farce/transaction/wrapper.rb +14 -0
  90. data/lib/farce/tree_map.rb +1 -1
  91. data/lib/farce/unsafe/lfu_map.rb +2 -0
  92. data/lib/farce/unsafe/lru_map.rb +2 -0
  93. data/lib/farce/unsafe/tree_map.rb +2 -0
  94. data/lib/farce/unshareable.rb +8 -19
  95. data/lib/farce/unshared/atom.rb +3 -0
  96. data/lib/farce/unshared/lfu_map.rb +2 -0
  97. data/lib/farce/unshared/lru_map.rb +2 -0
  98. data/lib/farce/unshared/map.rb +3 -0
  99. data/lib/farce/unshared/pool.rb +44 -0
  100. data/lib/farce/unshared/scheduler.rb +89 -0
  101. data/lib/farce/unshared/set.rb +1 -0
  102. data/lib/farce/unshared/sorted_set.rb +1 -0
  103. data/lib/farce/unshared/tree_map.rb +5 -1
  104. data/lib/farce/unshared/vector.rb +3 -0
  105. data/lib/farce/unshared/weak_atom.rb +5 -1
  106. data/lib/farce/unshared/weak_key_map.rb +3 -0
  107. data/lib/farce/unshared/weak_map.rb +3 -0
  108. data/lib/farce/unshared/weak_set.rb +1 -0
  109. data/lib/farce/unshared/weak_value_map.rb +3 -0
  110. data/lib/farce/vector.rb +44 -19
  111. data/lib/farce/version.rb +1 -1
  112. data/lib/farce.rb +20 -25
  113. metadata +18 -3
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Local
7
+ # A shareable handle with an independent task scheduler in each scope.
8
+ #
9
+ # {.new} leaves each scheduler unstarted. Launch it with {#launch_thread}, or
10
+ # install it with `Fiber.set_scheduler`. {.create} starts a worker thread in
11
+ # each scope when that scope is first used. The default scope is `:ractor`.
12
+ #
13
+ # Tasks retain their blocks and argument references. {#execute} runs directly
14
+ # in the calling fiber. A fiber scheduler
15
+ # constructor must be convertible to a shareable proc for use in each Ractor.
16
+ # {#close} closes the current scope. {#close_all} closes all scopes and prevents
17
+ # new schedulers from starting. {.current} returns an {Unshared::Scheduler}.
18
+ #
19
+ # @example Start one worker in each Ractor
20
+ # scheduler = Farce::Local::Scheduler.create
21
+ # results = Farce::Unshared::Queue.new
22
+ # scheduler.schedule { results.push(:done) }
23
+ # results.pop # => :done
24
+ # scheduler.close_all
25
+ class Scheduler < Abstract::Scheduler
26
+ include Shareable::Unfreezable
27
+ include Scoped
28
+ include Internal::LocalExecutorMethods
29
+
30
+ # Returns the current thread's unshared scheduler, or nil when none is installed.
31
+ # @param (see Abstract::Scheduler.current)
32
+ # @return [Unshared::Scheduler, nil]
33
+ def self.current(**) = Unshared::Scheduler.current(**)
34
+
35
+ # Starts a worker in this scope and retains the launch settings for other scopes.
36
+ # Additional options and the block are forwarded to {#initialize}.
37
+ # @param executor [Class, nil] Thread or a custom executor that stays in each Ractor.
38
+ # @param name [String, nil] Optional worker name.
39
+ # @param priority [Integer, nil] Optional worker thread priority.
40
+ # @yieldreturn [Object] Fiber scheduler to install in each worker.
41
+ # @return [Scheduler]
42
+ def self.create(executor = Thread, name: nil, priority: nil, **, &)
43
+ raise ArgumentError, "Local schedulers cannot launch Ractor workers" if executor && executor <= Ractor
44
+ worker = [executor || Thread, name && -String(name), priority && Integer(priority)].freeze
45
+ new(**, worker:, &)
46
+ end
47
+
48
+ # Creates a handle whose scoped schedulers remain unstarted.
49
+ # The constructor block builds the fiber scheduler when a scope is launched or installed.
50
+ # @!macro scopes
51
+ # @overload initialize(capacity: 1024, backend: Farce.config.io_backend, scope: :ractor, &constructor)
52
+ # @param capacity [Integer, nil] Pending-task capacity, or nil for an unbounded queue in each scope.
53
+ # @param backend [Symbol] IO backend for the built-in scheduler.
54
+ # @param scope [Symbol] The scheduler scope. Defaults to :ractor.
55
+ # @yieldreturn [Object] Fiber scheduler to install in each scope.
56
+ def initialize(capacity: 1024, backend: Internal::FROZEN_CONFIG.io_backend, scope: :ractor,
57
+ worker: nil, &constructor)
58
+ @constructor = prepare_constructor(constructor || Internal::FROZEN_CONFIG.fiber_scheduler_constructor)
59
+ @backend = backend
60
+ @worker = worker
61
+ @schedulers = Strict::Map.new
62
+ @closed_all = Internal::Flag.new(false)
63
+ @initialization_key = Object.new.freeze
64
+ @registration_key = Object.new.freeze
65
+ super(scope:, capacity: capacity && Integer(capacity), backend:)
66
+ end
67
+
68
+ # Enqueue a task in the current scope, preserving its block and arguments.
69
+ # `mode` is ignored when `auto_local` is true. Otherwise, only local and smart modes are accepted.
70
+ # @overload schedule(*args, mode: :smart, auto_local: true)
71
+ # @param (see Farce::Abstract::Executor#schedule)
72
+ # @return [self]
73
+ def schedule(...) = enqueue_task(false, ...)
74
+
75
+ # (see Abstract::Scheduler#try_schedule)
76
+ def try_schedule(...) = enqueue_task(true, ...)
77
+
78
+ # Starts a worker for the current scope. Other scopes retain their creation behavior.
79
+ # @param executor [Class, nil] Thread or a custom executor that stays in this Ractor.
80
+ # @param name [String, nil] Optional worker name.
81
+ # @param priority [Integer, nil] Optional worker thread priority.
82
+ # @return [Thread, Object] The worker.
83
+ # @raise [ArgumentError] If a Ractor executor is supplied.
84
+ def launch(executor = Thread, name: nil, priority: nil)
85
+ raise ArgumentError, "Local schedulers cannot launch Ractor workers" if executor && executor <= Ractor
86
+ raise "Scheduler may not be reused" if @closed_all.value
87
+ active_scoped_value.launch(executor, name:, priority:)
88
+ end
89
+
90
+ # Closes the current scope and rejects new submissions there.
91
+ # @return [self]
92
+ def close
93
+ current_scheduler&.close
94
+ self
95
+ end
96
+
97
+ # Requests shutdown in every scope and Ractor and rejects future scopes.
98
+ # Does not wait for queued or running tasks to finish.
99
+ # @return [self]
100
+ def close_all
101
+ return self unless @closed_all.compare_and_set(false, true)
102
+ @schedulers.each_pair do |scheduler, queue|
103
+ queue.push([scheduler, :close].freeze, clock: 0)
104
+ rescue ClosedQueueError
105
+ # The owning Ractor may already have shut down its supervisor.
106
+ nil
107
+ end
108
+ close
109
+ end
110
+
111
+ # @return [Symbol] The current scope's scheduler state.
112
+ def state = current_scheduler&.state || :closed
113
+
114
+ # @return [Exception, nil] The current scope's scheduler error.
115
+ def error = current_scheduler&.error
116
+
117
+ # @return [Boolean] Whether the current scope rejects new tasks.
118
+ def closed? = @closed_all.value || scoped_value.closed?
119
+
120
+ # @return [Boolean] Whether the current scope is dispatching tasks.
121
+ def alive? = current_scheduler&.alive? || false
122
+
123
+ # @return [Ractor] The current scope's owning Ractor.
124
+ def owner = Ractor.current
125
+
126
+ # @param value [Ractor] The current Ractor.
127
+ # @raise [ArgumentError] If another owner is supplied.
128
+ def owner=(value)
129
+ raise ArgumentError, "owner must be the current Ractor" unless value == Ractor.current
130
+ end
131
+
132
+ # @return [Boolean] Whether a fiber scheduler constructor was supplied.
133
+ def wraps_external? = !@constructor.nil?
134
+
135
+ # @return [Object, nil] The fiber scheduler in the current scope, when supplied by a constructor.
136
+ def external_scheduler = current_scheduler&.external_scheduler
137
+
138
+ # @api private
139
+ def inspect_with(inspector)
140
+ inspector.object_group(self) do
141
+ inspector.attribute(:scope, scope)
142
+ inspector.attribute(:state, state)
143
+ inspector.attribute(:backend, @backend)
144
+ yield if block_given?
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ def enqueue_task(non_block, *, mode: :smart, auto_local: true, &block)
151
+ raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if @closed_all.value
152
+ scheduler = scoped_value
153
+ raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if scheduler.closed?
154
+ raise LocalJumpError, "Cannot schedule without a block" unless block
155
+
156
+ validate_local_mode(mode, auto_local)
157
+ register_scheduler(scheduler)
158
+
159
+ submitted =
160
+ if non_block
161
+ scheduler.try_schedule(*, mode:, auto_local:, &block)
162
+ else
163
+ scheduler.schedule(*, mode:, auto_local:, &block)
164
+ end
165
+
166
+ submitted && self
167
+ end
168
+
169
+ def current_scheduler
170
+ return scoped_value unless @closed_all.value
171
+ scheduler = Internal::Storage[self, scope:]
172
+ scheduler&.close
173
+ scheduler
174
+ end
175
+
176
+ # Worker setup can yield. Initialize once when fibers share a scope.
177
+ def scoped_value
178
+ storage = Internal::Storage.scope(scope)
179
+ scheduler = storage[self]
180
+ return scheduler if scheduler
181
+
182
+ initializers = Internal::Storage.store_if_absent(@initialization_key) { Internal::Storage::ThreadSafe.new }
183
+ initializers.store_if_absent(storage) { Internal::LeaseInitialization.new }.fetch { super }
184
+ end
185
+
186
+ def active_scoped_value
187
+ scheduler = scoped_value
188
+ register_scheduler(scheduler)
189
+ scheduler
190
+ end
191
+
192
+ def register_scheduler(scheduler)
193
+ registrations = Internal::Storage.store_if_absent(@registration_key) { Internal::Storage::ThreadSafe.new }
194
+ registrations.store_if_absent(scheduler) do
195
+ queue, reference = Internal::ThreadPoolSupervisor.current.close_handle(scheduler)
196
+ @schedulers[reference] = queue
197
+ true
198
+ end
199
+ scheduler.close if @closed_all.value
200
+ end
201
+
202
+ def new_scoped_value(**)
203
+ scheduler = Unshared::Scheduler.new(**, &@constructor)
204
+ if @worker && !@closed_all.value
205
+ register_scheduler(scheduler)
206
+ executor, name, priority = @worker
207
+ scheduler.launch(executor, name:, priority:) unless @closed_all.value
208
+ end
209
+ scheduler.close if @closed_all.value
210
+ scheduler
211
+ end
212
+
213
+ def set_scheduler!(...)
214
+ raise "Scheduler may not be reused" if @closed_all.value
215
+ active_scoped_value.set_scheduler!(...)
216
+ end
217
+ end
218
+ end
219
+ 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)