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.
Files changed (139) 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/ext/farce/atom.c +37 -22
  9. data/ext/farce/bounded_map.c +17 -13
  10. data/ext/farce/containers.h +15 -0
  11. data/ext/farce/counter.c +13 -18
  12. data/ext/farce/exchanger.c +5 -1
  13. data/ext/farce/farce.c +1 -1
  14. data/ext/farce/flag.c +39 -3
  15. data/ext/farce/io.c +3 -3
  16. data/ext/farce/lock.c +17 -23
  17. data/ext/farce/map.c +88 -66
  18. data/ext/farce/priority_queue.c +55 -43
  19. data/ext/farce/queue.c +10 -4
  20. data/ext/farce/reactor.c +52 -25
  21. data/ext/farce/reactor.h +1 -1
  22. data/ext/farce/signal.c +5 -1
  23. data/ext/farce/transaction.c +20 -24
  24. data/ext/farce/tree_map.c +33 -18
  25. data/ext/farce/trie.c +81 -43
  26. data/ext/farce/unshareable.c +168 -19
  27. data/ext/farce/unshared_io_pool.h +21 -12
  28. data/ext/farce/unshared_signal.c +13 -2
  29. data/ext/farce/unshared_wait.h +3 -0
  30. data/ext/farce/vector.c +82 -18
  31. data/ext/farce/weak_map.c +75 -63
  32. data/lib/farce/_yard/macros.rb +18 -3
  33. data/lib/farce/abstract/executor.rb +130 -0
  34. data/lib/farce/abstract/flag.rb +73 -0
  35. data/lib/farce/abstract/pool.rb +334 -0
  36. data/lib/farce/abstract/queue.rb +4 -3
  37. data/lib/farce/abstract/scheduler.rb +539 -93
  38. data/lib/farce/abstract/set.rb +17 -5
  39. data/lib/farce/abstract/sorted_set.rb +2 -2
  40. data/lib/farce/abstract/timer_queue.rb +49 -10
  41. data/lib/farce/abstract/vector.rb +510 -28
  42. data/lib/farce/atom.rb +1 -1
  43. data/lib/farce/class_mirror.rb +20 -7
  44. data/lib/farce/config.rb +7 -1
  45. data/lib/farce/engine/jruby/signal.rb +7 -4
  46. data/lib/farce/engine/jvm/flag.rb +15 -3
  47. data/lib/farce/engine/ruby/3.4/farce.so +0 -0
  48. data/lib/farce/engine/ruby/4.0/farce.so +0 -0
  49. data/lib/farce/engine/ruby/shared/require_hook.rb +18 -0
  50. data/lib/farce/engine/ruby.rb +7 -0
  51. data/lib/farce/engine/shared/main_scheduler.rb +2 -2
  52. data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
  53. data/lib/farce/engine/shared/proxy_owner.rb +1 -1
  54. data/lib/farce/engine/shared/unshared_atom.rb +9 -8
  55. data/lib/farce/engine/shared/unshared_vector.rb +31 -4
  56. data/lib/farce/engine/shared/vector.rb +8 -0
  57. data/lib/farce/engine/shared/weak_map/index.rb +14 -1
  58. data/lib/farce/engine/shared.rb +9 -0
  59. data/lib/farce/engine/truffleruby/native/flag.rb +15 -3
  60. data/lib/farce/envelope.rb +54 -7
  61. data/lib/farce/exchanger.rb +1 -1
  62. data/lib/farce/flag.rb +2 -0
  63. data/lib/farce/integrations/active_support.rb +1 -1
  64. data/lib/farce/integrations/bson.rb +1 -1
  65. data/lib/farce/integrations/cbor.rb +1 -1
  66. data/lib/farce/integrations/concurrent.rb +3 -3
  67. data/lib/farce/integrations/date.rb +8 -0
  68. data/lib/farce/integrations/dry_types.rb +7 -4
  69. data/lib/farce/integrations/json.rb +2 -2
  70. data/lib/farce/integrations/msgpack.rb +1 -1
  71. data/lib/farce/integrations/oj.rb +3 -4
  72. data/lib/farce/integrations/psych.rb +1 -1
  73. data/lib/farce/integrations/ractor_sharing.rb +1 -1
  74. data/lib/farce/integrations/sorted_set.rb +1 -1
  75. data/lib/farce/integrations/weakref.rb +9 -1
  76. data/lib/farce/integrations/yajl.rb +3 -3
  77. data/lib/farce/integrations.rb +33 -23
  78. data/lib/farce/internal/flag_waiting.rb +30 -0
  79. data/lib/farce/internal/freeze.rb +8 -3
  80. data/lib/farce/internal/lease_initialization.rb +1 -1
  81. data/lib/farce/internal/local_executor_methods.rb +33 -0
  82. data/lib/farce/internal/local_scheduled_task.rb +27 -0
  83. data/lib/farce/internal/map_value_modes.rb +1 -1
  84. data/lib/farce/internal/scheduled_task.rb +1 -1
  85. data/lib/farce/internal/scheduler_lifecycle.rb +1 -1
  86. data/lib/farce/internal/thread_pool_supervisor.rb +45 -0
  87. data/lib/farce/internal/unshared_pool_worker.rb +14 -0
  88. data/lib/farce/internal.rb +14 -0
  89. data/lib/farce/lazy.rb +4 -4
  90. data/lib/farce/lazy_ref.rb +3 -3
  91. data/lib/farce/lfu_map.rb +1 -1
  92. data/lib/farce/local/flag.rb +4 -0
  93. data/lib/farce/local/pool.rb +111 -0
  94. data/lib/farce/local/scheduler.rb +219 -0
  95. data/lib/farce/local/scoped.rb +1 -1
  96. data/lib/farce/lru_map.rb +1 -1
  97. data/lib/farce/map.rb +2 -2
  98. data/lib/farce/mode_manager/smart_rule.rb +138 -0
  99. data/lib/farce/mode_manager.rb +12 -3
  100. data/lib/farce/molecule.rb +2 -2
  101. data/lib/farce/pool.rb +2 -290
  102. data/lib/farce/port.rb +4 -4
  103. data/lib/farce/priority_queue.rb +1 -1
  104. data/lib/farce/proxy/register.rb +6 -0
  105. data/lib/farce/proxy/supervisor.rb +1 -1
  106. data/lib/farce/proxy.rb +9 -4
  107. data/lib/farce/queue.rb +1 -1
  108. data/lib/farce/scheduler.rb +1 -502
  109. data/lib/farce/set.rb +1 -1
  110. data/lib/farce/sorted_set.rb +1 -1
  111. data/lib/farce/task.rb +285 -0
  112. data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
  113. data/lib/farce/timer_queue.rb +20 -6
  114. data/lib/farce/transaction/vector.rb +43 -9
  115. data/lib/farce/transaction/wrapper.rb +14 -0
  116. data/lib/farce/tree_map.rb +1 -1
  117. data/lib/farce/unsafe/lfu_map.rb +2 -0
  118. data/lib/farce/unsafe/lru_map.rb +2 -0
  119. data/lib/farce/unsafe/tree_map.rb +2 -0
  120. data/lib/farce/unshareable.rb +8 -19
  121. data/lib/farce/unshared/atom.rb +3 -0
  122. data/lib/farce/unshared/lfu_map.rb +2 -0
  123. data/lib/farce/unshared/lru_map.rb +2 -0
  124. data/lib/farce/unshared/map.rb +3 -0
  125. data/lib/farce/unshared/pool.rb +44 -0
  126. data/lib/farce/unshared/scheduler.rb +89 -0
  127. data/lib/farce/unshared/set.rb +1 -0
  128. data/lib/farce/unshared/sorted_set.rb +1 -0
  129. data/lib/farce/unshared/tree_map.rb +5 -1
  130. data/lib/farce/unshared/vector.rb +3 -0
  131. data/lib/farce/unshared/weak_atom.rb +5 -1
  132. data/lib/farce/unshared/weak_key_map.rb +3 -0
  133. data/lib/farce/unshared/weak_map.rb +3 -0
  134. data/lib/farce/unshared/weak_set.rb +1 -0
  135. data/lib/farce/unshared/weak_value_map.rb +3 -0
  136. data/lib/farce/vector.rb +44 -19
  137. data/lib/farce/version.rb +1 -1
  138. data/lib/farce.rb +20 -25
  139. metadata +17 -2
data/lib/farce/task.rb ADDED
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ # A unit of work that executes once and retains its result or failure.
7
+ #
8
+ # ```ruby
9
+ # # You can create and schedule a task with one call:
10
+ # task = Farce::Task.schedule { 1 + 2 }
11
+ # task.value # => 3
12
+ #
13
+ # # There is also a shorthand for running a task on the global Ractor pool:
14
+ # task = Farce::Task.in_parallel { sleep 1 }
15
+ # task.join
16
+ # task.alive? # => false
17
+ #
18
+ # # schedule accepts a custom scheduler:
19
+ # scheduler = Farce::Scheduler.create
20
+ # task = Farce::Task.schedule(scheduler:) { 1 + 2 }
21
+ #
22
+ # # or alternatively you can manually create a task and hand it to the scheduler
23
+ # task = Farce::Task.new { 1 + 2 }
24
+ # scheduler.schedule(&task)
25
+ #
26
+ # # or execute it manually
27
+ # task = Farce::Task.new { Ractor.current.name.upcase }
28
+ # Ractor.new(task, name: "Herbert") { it.call }
29
+ # task.value # => "HERBERT"
30
+ # ```
31
+ class Task
32
+ include Abstract::Value
33
+ include Internal::Freeze::Delegated
34
+ include Internal::MarshalSupport::Reject
35
+
36
+ # Raised when an operation is attempted on a task that is in an incorrect state.
37
+ InvalidStateError = Class.new(RuntimeError)
38
+
39
+ # @!macro [new] task_schedule
40
+ # @!macro modes
41
+ # @param args [Array] the arguments to pass to the task
42
+ # @param mode [Symbol] the mode for arguments and the successful result
43
+ # Arguments can be overridden by {#add_argument}.
44
+ # @yield [*args] the block to execute with the given arguments
45
+ # @yieldparam args [Array] the arguments passed to the block
46
+ # @yieldreturn [Object] the result of the block execution, will become the task's {#value}
47
+ # @raise ArgumentError if the provided mode is invalid
48
+ # @raise LocalJumpError if no callback is provided
49
+
50
+ # @overload in_parallel(*args, mode: :smart)
51
+ # Creates a task and schedules it for execution on the {Farce.in_parallel globally shared executor pool}.
52
+ # @!macro task_schedule
53
+ # @return [Task] the created task
54
+ def self.in_parallel(*, **, &) = enqueue(false, Farce.in_parallel, *, auto_local: false, **, &)
55
+
56
+ # @overload on_main(*args, mode: :smart, auto_local: true)
57
+ # Creates a task and schedules it for execution on the {Farce.on_main main scheduler}.
58
+ # If `auto_local` is true (default), `mode` will be set to `:local` if the method is called from the main Ractor.
59
+ #
60
+ # @!macro task_schedule
61
+ # @param auto_local [Boolean] whether to automatically switch to local mode if called from the main Ractor
62
+ # @return [Task] the created task
63
+ def self.on_main(*, **, &) = enqueue(false, Farce.on_main, *, **, &)
64
+
65
+ # @overload schedule(*args, mode: :smart, auto_local: true, scheduler: nil, &)
66
+ # Creates a task and schedules it for execution.
67
+ # A scheduler may optionally be provided.
68
+ #
69
+ # If no scheduler is provided, then it will look for a Ractor-local scheduler if mode is `:local` or `auto_local`
70
+ # is true. If none is found, one will be created automatically if `:mode` is `:local`, otherwise the
71
+ # {Farce.in_parallel globally shared executor pool} will be used.
72
+ #
73
+ # The scheduler may be a {Abstract::Executor executor}, or a Fiber scheduler.
74
+ #
75
+ # @!macro task_schedule
76
+ # @param scheduler [Abstract::Executor, Fiber::Scheduler, nil] the scheduler to use for execution
77
+ # @return [Task] the created task
78
+ def self.schedule(*, scheduler: nil, **, &) = enqueue(false, scheduler, *, **, &)
79
+
80
+ # @overload try_schedule(*args, mode: :smart, auto_local: true, scheduler: nil, &)
81
+ # Creates a task and attempts to schedule it for execution.
82
+ #
83
+ # Behaves exactly like {.schedule}, except it uses {Abstract::Executor#try_schedule} instead of
84
+ # {Abstract::Executor#schedule} if the scheduler is an executor, and that it returns `nil` if it fails to
85
+ # schedule the task.
86
+ #
87
+ # @!macro task_schedule
88
+ # @return [Task, nil] the created task, or `nil` if it failed to schedule
89
+ def self.try_schedule(*, scheduler: nil, **, &) = enqueue(true, scheduler, *, **, &)
90
+
91
+ # @api private
92
+ def self.enqueue(non_block, scheduler, *, mode: :smart, auto_local: true, &) # :nodoc:
93
+ unless scheduler
94
+ scheduler = Internal.local_scheduler(create: mode == :local) if auto_local || mode == :local
95
+ unless scheduler
96
+ scheduler = Internal::ParallelScheduler
97
+ auto_local = false
98
+ end
99
+ end
100
+
101
+ if scheduler.is_a?(Abstract::Executor)
102
+ mode = :local if auto_local && scheduler.local?(wait: false)
103
+ task = new(*, mode:, &)
104
+
105
+ if non_block
106
+ return unless scheduler.try_schedule(mode:, auto_local: false, &task)
107
+ else
108
+ scheduler.schedule(mode:, auto_local: false, &task)
109
+ end
110
+ else
111
+ task = new(*, mode: :local, &)
112
+ scheduler.fiber(&task)
113
+ end
114
+
115
+ task
116
+ end
117
+ private_class_method :enqueue
118
+
119
+ # @return [Symbol] the default mode used for task arguments and the return value
120
+ attr_reader :mode
121
+
122
+ # Creates a new task with the given arguments and callback.
123
+ #
124
+ # This does not schedule the task for execution, nor does it associate it with any executor.
125
+ #
126
+ # In fact, the task object itself is a proc-like object, that can be {call called} like a regular proc,
127
+ # but will track its execution state and result.
128
+ #
129
+ # @example Using a Task without an Executor
130
+ # task = Farce::Task.new(1, 2) { |a, b| a + b }
131
+ #
132
+ # task.alive? # => true
133
+ # task.call == task # => true
134
+ # task.alive? # => false
135
+ # task.value # => 3
136
+ #
137
+ # @example Passing a task to an executor
138
+ # task = Farce::Task.new { puts "Hello, world!" }
139
+ # Farce.in_parallel(&task)
140
+ # task.join # waits for the task to complete
141
+ #
142
+ # @!macro task_schedule
143
+ def initialize(*args, mode: :smart, &callback)
144
+ raise ArgumentError, "invalid mode: #{mode}" unless MODES.include?(mode)
145
+ raise LocalJumpError, "no callback provided" unless block_given?
146
+
147
+ @mode = mode
148
+ @ractor = Strict::Atom.new
149
+ @state = Strict::Atom.new(:initialized)
150
+
151
+ if mode == :local
152
+ @arguments = args
153
+ @callback = callback
154
+ @manager = nil
155
+ @ractor.value = Ractor.current
156
+ @result = Unshared::Atom.new
157
+ else
158
+ @arguments = Strict::Vector.new
159
+ @callback = Ractor.shareable?(callback) ? callback : Ractor.shareable_proc(&callback)
160
+ @manager = ModeManager.new(mode: mode)
161
+ @result = Strict::Atom.new
162
+ args.each { add_argument(it) }
163
+ end
164
+
165
+ __freeze__
166
+ end
167
+
168
+ # Adds an argument to the task.
169
+ # Can only be called before the task is frozen and before {#call} is invoked.
170
+ #
171
+ # Allows for arguments to use different modes than the return value (or other arguments).
172
+ # Mode is ignored for {#local? local} tasks.
173
+ #
174
+ # For non-local tasks, `mode` may only be set to `:local` for arguments added by the Ractor that will
175
+ # {#call execute} the task.
176
+ #
177
+ # @example A task using `move` for the return value, but `copy` for its argument
178
+ # task = Farce::Task.new(mode: :move) { |arg| arg.upcase }
179
+ # task.add_argument("hello", mode: :copy)
180
+ # Farce.in_parallel(&task)
181
+ # task.value # => "HELLO"
182
+ #
183
+ # @param argument [BasicObject] the argument to add to the task
184
+ # @param mode [Symbol, nil] the mode to use for the argument, or `nil` to use the task's default mode
185
+ # @raise ArgumentError if the provided mode is invalid
186
+ # @raise FrozenError if the task has already been frozen or called
187
+ # @raise InvalidStateError if trying to add a local argument from more than one ractor
188
+ # @return [self] the task itself, for chaining
189
+ def add_argument(argument, mode: nil)
190
+ check_frozen!
191
+
192
+ unless local?
193
+ raise ArgumentError, "invalid mode: #{mode}" unless mode.nil? || MODES.include?(mode)
194
+ raise InvalidStateError, "cannot add local argument to a non-local task" if mode == :local && !claim_for_ractor?
195
+ argument = @manager.wrap(argument, mode:)
196
+ end
197
+
198
+ @arguments << argument
199
+ self
200
+ end
201
+
202
+ alias << add_argument
203
+
204
+ # @return [Boolean] whether the task is currently alive (initialized or running)
205
+ def alive? = alive_state?(@state.value)
206
+
207
+ # Executes the task by invoking its callback with the added arguments.
208
+ # @raise InvalidStateError if the task has already been claimed by another ractor or has already been called
209
+ # @return [self] the task itself, for chaining
210
+ def call
211
+ raise InvalidStateError, "task already claimed by another ractor" unless claim_for_ractor?
212
+ raise InvalidStateError, "call may only be called once" unless @state.compare_and_set(:initialized, :running)
213
+ begin
214
+ arguments = local? ? @arguments.freeze : @arguments.freeze.to_a.map! { @manager.unwrap(it) }
215
+ result = @callback.call(*arguments)
216
+ @result.value = local? ? result : @manager.wrap(result)
217
+ @state.value = :finished
218
+ rescue Exception => e # rubocop:disable Lint/RescueException
219
+ # Every callback failure must release waiters, including ScriptError and SystemExit.
220
+ begin
221
+ @result.value = e
222
+ rescue Ractor::Error
223
+ @result.value = [RuntimeError, -"#{e.class}: #{e.message}", Ractor.make_shareable(e.backtrace)].freeze
224
+ end
225
+ @state.value = :error
226
+ ensure
227
+ if @state.value == :running
228
+ @result.value = [RuntimeError, -"task execution aborted"].freeze
229
+ @state.value = :error
230
+ end
231
+ end
232
+
233
+ self
234
+ end
235
+
236
+ # Waits for the task to finish, with an optional timeout.
237
+ # @param timeout [Numeric, nil] the maximum time to wait, or `nil` to wait indefinitely
238
+ # @return [self, nil] the task itself if it finished within the timeout, or `nil` otherwise
239
+ def join(timeout = nil)
240
+ return unless @state.wait_while(timeout:) { alive_state?(it) }
241
+ self
242
+ end
243
+
244
+ # @return [Boolean] whether the task is local (i.e., its mode is `:local`)
245
+ def local? = @mode == :local
246
+
247
+ # Waits for the task to finish and returns its value, with an optional timeout.
248
+ # If the task raises an error, it will be propagated.
249
+ #
250
+ # @param timeout [Numeric, nil] the maximum time to wait, or `nil` to wait indefinitely
251
+ # @return [BasicObject, nil]
252
+ # the value returned by the task's callback, or `nil` if the task did not finish within the timeout
253
+ # @raise Exception if the task raises an error
254
+ # @raise RuntimeError with the original class, message, and backtrace if the exception cannot be shared
255
+ # @raise RuntimeError if execution is aborted before producing a result or exception
256
+ def value(timeout = nil)
257
+ case @state.wait_while(timeout:) { alive_state?(it) }
258
+ when :finished then local? ? @result.value : @manager.unwrap(@result.value)
259
+ when :error then raise(*@result.value)
260
+ end
261
+ end
262
+
263
+ # Converts the task to a Proc object.
264
+ # If the task is Ractor-shareable, then so is the returned Proc.
265
+ # @return [Proc] a Proc that calls the task's `call` method
266
+ def to_proc
267
+ return method(:call).to_proc if local?
268
+ task = self
269
+ Ractor.make_shareable(-> { task.call })
270
+ end
271
+
272
+ private
273
+
274
+ def alive_state?(state) = state == :initialized || state == :running
275
+
276
+ def claim_for_ractor?
277
+ current_ractor = Ractor.current
278
+ @ractor.compare_and_set(nil, current_ractor)
279
+ @ractor.value == current_ractor
280
+ end
281
+
282
+ define_method(:__freeze__, Kernel.instance_method(:freeze))
283
+ def freeze_backend = @arguments
284
+ end
285
+ end
@@ -3,11 +3,11 @@
3
3
  # warn_indent: true
4
4
 
5
5
  module Farce
6
- # A scheduler that starts a new thread for each scheduled task. Executes synchronous tasks inline.
6
+ # A task executor that starts a new thread for each scheduled task. Executes synchronous tasks inline.
7
7
  # Creating an instance starts no threads. Blocks and arguments stay local to the caller.
8
- # Tasks are not tracked, and closing this stateless scheduler has no effect.
8
+ # Tasks are not tracked, and closing this stateless executor has no effect.
9
9
  # Default instances support Marshal. Instances with a custom thread factory reject dumping.
10
- class ThreadScheduler < Farce::Abstract::Scheduler
10
+ class ThreadExecutor < Farce::Abstract::Executor
11
11
  include Shareable::Immutable
12
12
  include Internal::Inspect
13
13
 
@@ -18,7 +18,7 @@ module Farce
18
18
 
19
19
  # @api private
20
20
  def marshal_dump
21
- raise TypeError, "scheduler factories cannot be marshaled" if @factory
21
+ raise TypeError, "thread factories cannot be marshaled" if @factory
22
22
  [1]
23
23
  end
24
24
 
@@ -32,7 +32,7 @@ module Farce
32
32
  #
33
33
  # @overload schedule(*args, **options)
34
34
  # @param args [Array<Object>] Arguments passed to the block.
35
- # @param options [Hash] Scheduler options are ignored. All tasks use local arguments.
35
+ # @param options [Hash] Execution options are ignored. All tasks use local arguments.
36
36
  # @yield [*args] The task to run.
37
37
  # @return [self]
38
38
  def schedule(*, **, &)
@@ -45,7 +45,7 @@ module Farce
45
45
  #
46
46
  # @overload execute(*args, **options)
47
47
  # @param args [Array<Object>] Arguments passed to the block.
48
- # @param options [Hash] Scheduler options are ignored. All tasks use local arguments.
48
+ # @param options [Hash] Execution options are ignored. All tasks use local arguments.
49
49
  # @yield [*args] The task to run.
50
50
  # @return [self]
51
51
  def execute(*, **)
@@ -57,7 +57,7 @@ module Farce
57
57
  # @return [self] Does not wait for or stop task threads.
58
58
  def close = self
59
59
 
60
- # @return [false] This stateless scheduler is always available.
60
+ # @return [false] This stateless executor is always available.
61
61
  def closed? = false
62
62
 
63
63
  # @return [Symbol] Always `:running`.
@@ -4,14 +4,14 @@
4
4
 
5
5
  module Farce
6
6
  # A shareable timer queue with transfer modes for unshareable values.
7
- class TimerQueue < Abstract::TimerQueue
7
+ class TimerQueue < Farce::Abstract::TimerQueue
8
8
  include Internal::ManagedQueue
9
9
  include Shareable::Unfreezable
10
10
 
11
11
  # (see Farce::Abstract::TimerQueue#initialize)
12
12
  # @!macro modes
13
13
  # @param mode [Symbol] the transfer mode for unshareable values
14
- def initialize(capacity: nil, mode: :copy, track_age: false)
14
+ def initialize(capacity: nil, mode: :smart, track_age: false)
15
15
  @manager = ModeManager.new(mode:)
16
16
  super(capacity:, track_age:)
17
17
  end
@@ -20,16 +20,20 @@ module Farce
20
20
  # @!macro modes
21
21
  # @param mode [Symbol] the transfer mode for unshareable values
22
22
  def push(value, non_block = false, mode: nil, timeout: nil, **time_options) # rubocop:disable Style/OptionalBooleanParameter
23
- at = Clock.parse(time_options)
24
- push_to_storage(at, non_block, @manager.wrap(value, mode:), timeout:)
23
+ at = Clock.parse(time_options)
24
+ value = @manager.wrap(value, mode:)
25
+ entry = Entry.new(value, at, self)
26
+ push_to_storage(at, non_block, value, timeout:) && entry
25
27
  end
26
28
 
27
29
  # (see Farce::Abstract::TimerQueue#try_push)
28
30
  # @!macro modes
29
31
  # @param mode [Symbol] the transfer mode for unshareable values
30
32
  def try_push(value, mode: nil, **time_options)
31
- at = Clock.parse(time_options)
32
- return true if @queue.push(at, @manager.wrap(value, mode:))
33
+ at = Clock.parse(time_options)
34
+ value = @manager.wrap(value, mode:)
35
+ entry = Entry.new(value, at, self)
36
+ return entry if @queue.push(at, value)
33
37
  block_given? ? yield : false
34
38
  end
35
39
 
@@ -62,5 +66,15 @@ module Farce
62
66
  return @manager.unwrap(value) unless empty
63
67
  yield if block_given?
64
68
  end
69
+
70
+ private
71
+
72
+ def delete_from_storage(priority, value, compare_by_identity:)
73
+ if compare_by_identity && @manager.managed_envelope?(value)
74
+ @queue.delete_identity(priority, value)
75
+ else
76
+ super
77
+ end
78
+ end
65
79
  end
66
80
  end
@@ -18,7 +18,12 @@ module Farce
18
18
 
19
19
  # (see Abstract::Vector#[])
20
20
  # @see Abstract::Vector#[]
21
- def [](index) = access { unwrap_stored(@working[index]) }
21
+ def [](index, length = UNDEFINED)
22
+ access do
23
+ next unwrap_stored(@working[index]) if length.equal?(UNDEFINED) && !index.is_a?(Range)
24
+ slice(index, length)
25
+ end
26
+ end
22
27
 
23
28
  # (see Abstract::Vector#get)
24
29
  # @see Abstract::Vector#get
@@ -34,8 +39,12 @@ module Farce
34
39
 
35
40
  # (see Abstract::Vector#[]=)
36
41
  # @see Abstract::Vector#[]=
37
- def []=(index, value)
38
- store(index, value)
42
+ def []=(index, value, replacement = UNDEFINED)
43
+ if replacement.equal?(UNDEFINED) && !index.is_a?(Range)
44
+ store(index, value)
45
+ else
46
+ write { assign_vector_slice(index, value, replacement) }
47
+ end
39
48
  end
40
49
 
41
50
  # (see Abstract::Vector#store)
@@ -52,15 +61,24 @@ module Farce
52
61
 
53
62
  # (see Abstract::Vector#push)
54
63
  # @see Abstract::Vector#push
55
- def push(value, mode: nil)
56
- write { @working.push(wrap(value, mode:)) }
64
+ def push(value = UNDEFINED, *rest, mode: nil)
65
+ write do
66
+ if rest.empty? && !value.equal?(UNDEFINED)
67
+ @working.push(wrap(value, mode:))
68
+ else
69
+ push_values(value.equal?(UNDEFINED) ? rest : [value, *rest], mode:)
70
+ end
71
+ end
57
72
  self
58
73
  end
59
- alias << push
74
+
75
+ def <<(value) = push(value)
60
76
 
61
77
  # (see Abstract::Vector#pop)
62
78
  # @see Abstract::Vector#pop
63
- def pop = write { unwrap_stored(@working.pop) }
79
+ def pop(count = UNDEFINED)
80
+ write { count.equal?(UNDEFINED) ? unwrap_stored(@working.pop) : pop_values(count) }
81
+ end
64
82
 
65
83
  # (see Abstract::Vector#clear)
66
84
  # @see Abstract::Vector#clear
@@ -120,6 +138,17 @@ module Farce
120
138
  def internal_vector = @working
121
139
  def logical_value(value) = unwrap_stored(value)
122
140
 
141
+ def deletion_comparison(value)
142
+ return value unless @manager
143
+ @manager.wrap(value, mode: compare_by_identity? ? :local : :copy)
144
+ end
145
+
146
+ def deletion_match?(stored, expected)
147
+ return super unless @manager
148
+ unwrap_stored(stored)
149
+ @manager.same_value?(stored, expected, identity: compare_by_identity?)
150
+ end
151
+
123
152
  # Slice helpers return independent data rather than another participant.
124
153
  # Use an ordinary vector that remains usable after the attempt ends.
125
154
  def build_derived_vector(values)
@@ -130,10 +159,15 @@ module Farce
130
159
 
131
160
  def inspect_value(inspector, value) = super(inspector, value, @manager)
132
161
  def derived_storage(value) = wrap(value)
162
+ def prepare_vector_value(value, mode: nil) = wrap(value, mode:)
133
163
 
134
- Wrapper.inherit(self, :each_index, :reverse_each, :to_a, :deconstruct, :to_h, :join, :dig, :assoc,
164
+ Wrapper.inherit(self, :each_index, :reverse_each, :to_a, :to_ary, :deconstruct, :to_h, :join, :dig, :assoc,
135
165
  :rassoc, :at, :fetch, :fetch_values, :values_at, :slice, :rfind, :bsearch, :bsearch_index, :pack,
136
- :length, :count, :compare_by_identity?, :shareable_values?)
166
+ :length, :count, :compare_by_identity?, :shareable_values?, :flatten, :transpose, :product,
167
+ :combination, :repeated_combination, :permutation, :repeated_permutation, :<=>,
168
+ writes: %i[delete delete_if map! collect! keep_if select! filter! reject! compact! uniq!
169
+ reverse! rotate! shuffle! sort! sort_by! flatten! shift unshift prepend insert delete_at slice!
170
+ replace fill append])
137
171
  end
138
172
  end
139
173
  end
@@ -15,6 +15,20 @@ module Farce
15
15
  module Wrapper
16
16
  include Internal::MarshalSupport::Reject
17
17
 
18
+ # Freezing an attempt view must not freeze its staged data or participant.
19
+ module Unfreezable
20
+ def append_features(mod)
21
+ super
22
+ if mod.is_a?(Class)
23
+ mod.class_eval { undef freeze if method_defined?(:freeze) }
24
+ else
25
+ mod.extend(Unfreezable)
26
+ end
27
+ end
28
+ end
29
+ private_constant :Unfreezable
30
+ extend Unfreezable
31
+
18
32
  # Wrappers inherit abstract container classes for their interfaces, but
19
33
  # must not initialize another ordinary container. This constructor ends
20
34
  # Unshareable's super chain after binding the existing transaction snapshot.
@@ -20,7 +20,7 @@ module Farce
20
20
  # @param entries [Hash, Array<Array(BasicObject, BasicObject)>, Abstract::Map, #each, nil]
21
21
  # Optional initial entries for the map.
22
22
  # @param mode [Symbol] The default value transfer mode.
23
- def initialize(entries = nil, mode: :copy, normalize_keys: nil, **keyword_entries)
23
+ def initialize(entries = nil, mode: :smart, normalize_keys: nil, **keyword_entries)
24
24
  unless keyword_entries.empty?
25
25
  raise ArgumentError, "entries given as both positional and keyword arguments" unless entries.nil?
26
26
  entries = keyword_entries
@@ -10,6 +10,7 @@ module Farce
10
10
  # does not coordinate competing callers, including callers loading the same key.
11
11
  class LFUMap < Abstract::LFUMap
12
12
  include Unshareable
13
+ include Internal::Freeze::Delegated
13
14
 
14
15
  # @!method store_if_absent(key)
15
16
  # Read an existing value or construct and store a value for an absent key.
@@ -29,6 +30,7 @@ module Farce
29
30
  private
30
31
 
31
32
  def new_bounded_map(...) = Internal::LFUMap.new(...)
33
+ def freeze_backend = @map
32
34
  def new_key_locks(**) = nil
33
35
  def with_key_lock(_key) = yield
34
36
  end
@@ -10,6 +10,7 @@ module Farce
10
10
  # does not coordinate competing callers, including callers loading the same key.
11
11
  class LRUMap < Abstract::LRUMap
12
12
  include Unshareable
13
+ include Internal::Freeze::Delegated
13
14
 
14
15
  # @!method store_if_absent(key)
15
16
  # Read an existing value or construct and store a value for an absent key.
@@ -29,6 +30,7 @@ module Farce
29
30
  private
30
31
 
31
32
  def new_bounded_map(...) = Internal::LRUMap.new(...)
33
+ def freeze_backend = @map
32
34
  def new_key_locks(**) = nil
33
35
  def with_key_lock(_key) = yield
34
36
  end
@@ -8,12 +8,14 @@ module Farce
8
8
  # A {Abstract::TreeMap tree map} that is not thread-safe and cannot be shared between Ractors.
9
9
  class TreeMap < Abstract::TreeMap
10
10
  include Unshareable
11
+ include Internal::Freeze::Delegated
11
12
 
12
13
  undef_method :transaction_wrapper
13
14
 
14
15
  private
15
16
 
16
17
  def new_tree_map(...) = Internal::UnsafeTreeMap.new(...)
18
+ def freeze_backend = @map
17
19
  def new_key_locks = nil
18
20
  def with_key_lock(_key) = yield
19
21
  end
@@ -4,7 +4,7 @@
4
4
 
5
5
  module Farce
6
6
  # Mixin that makes sure instances cannot be shared between Ractors.
7
- # @note Mixin in this module will remove the `freeze` method from the class.
7
+ # Instances may be frozen without becoming Ractor-shareable.
8
8
  #
9
9
  # @example
10
10
  # class MyClass
@@ -12,26 +12,10 @@ module Farce
12
12
  # end
13
13
  #
14
14
  # object = MyClass.new
15
- # object.freeze # this would normally make it shareable
15
+ # object.freeze # Preserves the Ractor restrictions.
16
16
  #
17
17
  # Ractor.shareable?(object) # => false
18
18
  module Unshareable
19
- # @api private
20
- module UndefFreeze # :nodoc: all
21
- # @api private
22
- def append_features(mod)
23
- super
24
- if mod.is_a?(Class)
25
- mod.class_eval { undef freeze if method_defined?(:freeze) }
26
- else
27
- mod.extend(UndefFreeze)
28
- end
29
- end
30
- end
31
-
32
- private_constant :UndefFreeze
33
- extend UndefFreeze
34
-
35
19
  # Mixin for classes that cannot be shared across Ractors, but may be moved between them.
36
20
  module Movable
37
21
  include Unshareable
@@ -58,7 +42,12 @@ module Farce
58
42
  def pin_unshareable
59
43
  return unless Internal.native_ractors?
60
44
  case self
61
- when Movable then Internal::Unshareable.prevent_copyable(self) unless is_a?(Copyable)
45
+ when Movable
46
+ if is_a?(Copyable)
47
+ Internal::Unshareable.prevent_shareable(self)
48
+ else
49
+ Internal::Unshareable.prevent_copyable(self)
50
+ end
62
51
  when Copyable then Internal::Unshareable.prevent_movable(self)
63
52
  else Internal::Unshareable.pin_to_current_ractor(self)
64
53
  end
@@ -12,6 +12,7 @@ module Farce
12
12
  # atom.update { |items| items + [:job] }
13
13
  class Atom < Abstract::Atom
14
14
  include Unshareable
15
+ include Internal::Freeze::Delegated
15
16
 
16
17
  # @param value [BasicObject, nil] the initial value
17
18
  # @param compare_by_identity [Boolean] whether comparisons use object identity
@@ -19,6 +20,8 @@ module Farce
19
20
  @atom = Internal::UnsharedAtom.new(value, compare_by_identity:)
20
21
  super()
21
22
  end
23
+
24
+ private def freeze_backend = @atom
22
25
  end
23
26
  end
24
27
  end
@@ -7,10 +7,12 @@ module Farce
7
7
  # A thread-safe LFU map for mutable keys and values used within one Ractor.
8
8
  class LFUMap < Abstract::LFUMap
9
9
  include Unshareable
10
+ include Internal::Freeze::Delegated
10
11
 
11
12
  private
12
13
 
13
14
  def new_bounded_map(...) = Internal::LFUMap.new(...)
15
+ def freeze_backend = @map
14
16
 
15
17
  def new_key_locks(compare_keys_by_identity:)
16
18
  Internal::KeyLockMap.new(registry_class: Farce::Unshared::Map, compare_keys_by_identity:)
@@ -7,10 +7,12 @@ module Farce
7
7
  # A thread-safe LRU map for mutable keys and values used within one Ractor.
8
8
  class LRUMap < Abstract::LRUMap
9
9
  include Unshareable
10
+ include Internal::Freeze::Delegated
10
11
 
11
12
  private
12
13
 
13
14
  def new_bounded_map(...) = Internal::LRUMap.new(...)
15
+ def freeze_backend = @map
14
16
 
15
17
  def new_key_locks(compare_keys_by_identity:)
16
18
  Internal::KeyLockMap.new(registry_class: Farce::Unshared::Map, compare_keys_by_identity:)
@@ -12,6 +12,7 @@ module Farce
12
12
  # `dup` and `clone` copy entries into independent storage while sharing stored values.
13
13
  class Map < Abstract::ConcurrentMap
14
14
  include Unshareable
15
+ include Internal::Freeze::Delegated
15
16
 
16
17
  # @!macro map_initialization
17
18
  #
@@ -44,6 +45,8 @@ module Farce
44
45
  end
45
46
 
46
47
  Internal.prepare_map_access(self, :unshared)
48
+
49
+ private def freeze_backend = @map
47
50
  end
48
51
  end
49
52
  end