farce 0.1.0-x86_64-linux-gnu → 0.3.0-x86_64-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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +127 -1
- data/README.md +111 -17
- data/docs/gems/dry-types.md +8 -8
- data/docs/modes.md +75 -15
- data/docs/scopes.md +3 -1
- data/docs/variants.md +18 -0
- data/ext/farce/atom.c +37 -22
- data/ext/farce/bounded_map.c +17 -13
- data/ext/farce/containers.h +15 -0
- data/ext/farce/counter.c +13 -18
- data/ext/farce/exchanger.c +5 -1
- data/ext/farce/farce.c +1 -1
- data/ext/farce/flag.c +39 -3
- data/ext/farce/io.c +3 -3
- data/ext/farce/lock.c +17 -23
- data/ext/farce/map.c +88 -66
- data/ext/farce/priority_queue.c +55 -43
- data/ext/farce/queue.c +10 -4
- data/ext/farce/reactor.c +52 -25
- data/ext/farce/reactor.h +1 -1
- data/ext/farce/signal.c +5 -1
- data/ext/farce/transaction.c +20 -24
- data/ext/farce/tree_map.c +33 -18
- data/ext/farce/trie.c +81 -43
- data/ext/farce/unshareable.c +168 -19
- data/ext/farce/unshared_io_pool.h +21 -12
- data/ext/farce/unshared_signal.c +13 -2
- data/ext/farce/unshared_wait.h +3 -0
- data/ext/farce/vector.c +82 -18
- data/ext/farce/weak_map.c +75 -63
- data/lib/farce/_yard/macros.rb +18 -3
- data/lib/farce/abstract/executor.rb +130 -0
- data/lib/farce/abstract/flag.rb +73 -0
- data/lib/farce/abstract/pool.rb +334 -0
- data/lib/farce/abstract/queue.rb +4 -3
- data/lib/farce/abstract/scheduler.rb +539 -93
- data/lib/farce/abstract/set.rb +17 -5
- data/lib/farce/abstract/sorted_set.rb +2 -2
- data/lib/farce/abstract/timer_queue.rb +49 -10
- data/lib/farce/abstract/vector.rb +510 -28
- data/lib/farce/atom.rb +1 -1
- data/lib/farce/class_mirror.rb +20 -7
- data/lib/farce/config.rb +7 -1
- data/lib/farce/engine/jruby/signal.rb +7 -4
- data/lib/farce/engine/jvm/flag.rb +15 -3
- data/lib/farce/engine/ruby/3.4/farce.so +0 -0
- data/lib/farce/engine/ruby/4.0/farce.so +0 -0
- data/lib/farce/engine/ruby/shared/require_hook.rb +18 -0
- data/lib/farce/engine/ruby.rb +7 -0
- data/lib/farce/engine/shared/main_scheduler.rb +2 -2
- data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
- data/lib/farce/engine/shared/proxy_owner.rb +1 -1
- data/lib/farce/engine/shared/unshared_atom.rb +9 -8
- data/lib/farce/engine/shared/unshared_vector.rb +31 -4
- data/lib/farce/engine/shared/vector.rb +8 -0
- data/lib/farce/engine/shared/weak_map/index.rb +14 -1
- data/lib/farce/engine/shared.rb +9 -0
- data/lib/farce/engine/truffleruby/native/flag.rb +15 -3
- data/lib/farce/envelope.rb +54 -7
- data/lib/farce/exchanger.rb +1 -1
- data/lib/farce/flag.rb +2 -0
- data/lib/farce/integrations/active_support.rb +1 -1
- data/lib/farce/integrations/bson.rb +1 -1
- data/lib/farce/integrations/cbor.rb +1 -1
- data/lib/farce/integrations/concurrent.rb +3 -3
- data/lib/farce/integrations/date.rb +8 -0
- data/lib/farce/integrations/dry_types.rb +7 -4
- data/lib/farce/integrations/json.rb +2 -2
- data/lib/farce/integrations/msgpack.rb +1 -1
- data/lib/farce/integrations/oj.rb +3 -4
- data/lib/farce/integrations/psych.rb +1 -1
- data/lib/farce/integrations/ractor_sharing.rb +1 -1
- data/lib/farce/integrations/sorted_set.rb +1 -1
- data/lib/farce/integrations/weakref.rb +9 -1
- data/lib/farce/integrations/yajl.rb +3 -3
- data/lib/farce/integrations.rb +33 -23
- data/lib/farce/internal/flag_waiting.rb +30 -0
- data/lib/farce/internal/freeze.rb +8 -3
- data/lib/farce/internal/lease_initialization.rb +1 -1
- data/lib/farce/internal/local_executor_methods.rb +33 -0
- data/lib/farce/internal/local_scheduled_task.rb +27 -0
- data/lib/farce/internal/map_value_modes.rb +1 -1
- data/lib/farce/internal/scheduled_task.rb +1 -1
- data/lib/farce/internal/scheduler_lifecycle.rb +1 -1
- data/lib/farce/internal/thread_pool_supervisor.rb +45 -0
- data/lib/farce/internal/unshared_pool_worker.rb +14 -0
- data/lib/farce/internal.rb +14 -0
- data/lib/farce/lazy.rb +4 -4
- data/lib/farce/lazy_ref.rb +3 -3
- data/lib/farce/lfu_map.rb +1 -1
- data/lib/farce/local/flag.rb +4 -0
- data/lib/farce/local/pool.rb +111 -0
- data/lib/farce/local/scheduler.rb +219 -0
- data/lib/farce/local/scoped.rb +1 -1
- data/lib/farce/lru_map.rb +1 -1
- data/lib/farce/map.rb +2 -2
- data/lib/farce/mode_manager/smart_rule.rb +138 -0
- data/lib/farce/mode_manager.rb +12 -3
- data/lib/farce/molecule.rb +2 -2
- data/lib/farce/pool.rb +2 -290
- data/lib/farce/port.rb +4 -4
- data/lib/farce/priority_queue.rb +1 -1
- data/lib/farce/proxy/register.rb +6 -0
- data/lib/farce/proxy/supervisor.rb +1 -1
- data/lib/farce/proxy.rb +9 -4
- data/lib/farce/queue.rb +1 -1
- data/lib/farce/scheduler.rb +1 -502
- data/lib/farce/set.rb +1 -1
- data/lib/farce/sorted_set.rb +1 -1
- data/lib/farce/task.rb +285 -0
- data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
- data/lib/farce/timer_queue.rb +20 -6
- data/lib/farce/transaction/vector.rb +43 -9
- data/lib/farce/transaction/wrapper.rb +14 -0
- data/lib/farce/tree_map.rb +1 -1
- data/lib/farce/unsafe/lfu_map.rb +2 -0
- data/lib/farce/unsafe/lru_map.rb +2 -0
- data/lib/farce/unsafe/tree_map.rb +2 -0
- data/lib/farce/unshareable.rb +8 -19
- data/lib/farce/unshared/atom.rb +3 -0
- data/lib/farce/unshared/lfu_map.rb +2 -0
- data/lib/farce/unshared/lru_map.rb +2 -0
- data/lib/farce/unshared/map.rb +3 -0
- data/lib/farce/unshared/pool.rb +44 -0
- data/lib/farce/unshared/scheduler.rb +89 -0
- data/lib/farce/unshared/set.rb +1 -0
- data/lib/farce/unshared/sorted_set.rb +1 -0
- data/lib/farce/unshared/tree_map.rb +5 -1
- data/lib/farce/unshared/vector.rb +3 -0
- data/lib/farce/unshared/weak_atom.rb +5 -1
- data/lib/farce/unshared/weak_key_map.rb +3 -0
- data/lib/farce/unshared/weak_map.rb +3 -0
- data/lib/farce/unshared/weak_set.rb +1 -0
- data/lib/farce/unshared/weak_value_map.rb +3 -0
- data/lib/farce/vector.rb +44 -19
- data/lib/farce/version.rb +1 -1
- data/lib/farce.rb +20 -25
- metadata +17 -2
|
@@ -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
|
data/lib/farce/local/scoped.rb
CHANGED
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: :
|
|
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 `:
|
|
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: :
|
|
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
|
data/lib/farce/mode_manager.rb
CHANGED
|
@@ -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: :
|
|
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: :
|
|
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
|
|
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
|
data/lib/farce/molecule.rb
CHANGED
|
@@ -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 :
|
|
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 = :
|
|
35
|
+
def self.default_mode = :smart
|
|
36
36
|
|
|
37
37
|
# @!visibility private
|
|
38
38
|
def self.define_member(field)
|