farce 0.4.1-java → 0.5.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -2
  3. data/README.md +3 -1
  4. data/docs/benchmarks.md +37 -21
  5. data/docs/compatibility.md +13 -0
  6. data/docs/data-structures.md +37 -0
  7. data/docs/integrations.md +1 -0
  8. data/docs/introduction.md +19 -0
  9. data/docs/modes.md +2 -0
  10. data/docs/scheduling.md +35 -3
  11. data/docs/scopes.md +3 -1
  12. data/docs/shims.md +53 -0
  13. data/docs/utilities.md +31 -0
  14. data/docs/variants.md +9 -0
  15. data/lib/farce/abstract/concurrent_map.rb +122 -0
  16. data/lib/farce/abstract/counter.rb +35 -1
  17. data/lib/farce/abstract/executor.rb +30 -7
  18. data/lib/farce/abstract/flag.rb +12 -0
  19. data/lib/farce/abstract/pool.rb +158 -47
  20. data/lib/farce/abstract/priority_queue.rb +1 -1
  21. data/lib/farce/abstract/publisher.rb +189 -0
  22. data/lib/farce/abstract/queue.rb +29 -3
  23. data/lib/farce/abstract/scheduler.rb +41 -14
  24. data/lib/farce/abstract/timer_queue.rb +3 -3
  25. data/lib/farce/abstract/vector.rb +3 -1
  26. data/lib/farce/atom.rb +9 -2
  27. data/lib/farce/clock.rb +12 -0
  28. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  29. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  30. data/lib/farce/concurrent/event.rb +60 -0
  31. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  32. data/lib/farce/concurrent/local_var.rb +65 -0
  33. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  34. data/lib/farce/concurrent/tuple.rb +74 -0
  35. data/lib/farce/concurrent.rb +50 -0
  36. data/lib/farce/config.rb +112 -2
  37. data/lib/farce/counter.rb +0 -16
  38. data/lib/farce/engine/jruby/map.rb +8 -6
  39. data/lib/farce/engine/jruby/signal.rb +2 -2
  40. data/lib/farce/engine/jvm/farce.jar +0 -0
  41. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  42. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  43. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  44. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  45. data/lib/farce/engine/shared/exchanger.rb +2 -2
  46. data/lib/farce/engine/shared/queue.rb +43 -5
  47. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  48. data/lib/farce/engine/shared/strict_map.rb +5 -0
  49. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  50. data/lib/farce/engine/shared/unshared_vector.rb +8 -7
  51. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  52. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  53. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  54. data/lib/farce/engine/truffleruby/map.rb +8 -6
  55. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  56. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  57. data/lib/farce/integrations/concurrent.rb +16 -3
  58. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  59. data/lib/farce/internal/flag_waiting.rb +1 -1
  60. data/lib/farce/internal/lease_initialization.rb +1 -1
  61. data/lib/farce/internal/lease_pool_state.rb +1 -1
  62. data/lib/farce/internal/lease_state.rb +2 -2
  63. data/lib/farce/internal/local_executor_methods.rb +3 -3
  64. data/lib/farce/internal/map_value_modes.rb +9 -2
  65. data/lib/farce/internal/pool_settings.rb +9 -0
  66. data/lib/farce/internal/pool_worker.rb +34 -21
  67. data/lib/farce/internal/reservation_waiting.rb +1 -1
  68. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  69. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  70. data/lib/farce/internal.rb +8 -8
  71. data/lib/farce/lazy.rb +3 -0
  72. data/lib/farce/lazy_ref.rb +3 -0
  73. data/lib/farce/local/lease.rb +2 -9
  74. data/lib/farce/local/lease_map.rb +2 -9
  75. data/lib/farce/local/pool.rb +38 -5
  76. data/lib/farce/local/publisher.rb +30 -0
  77. data/lib/farce/local/scheduler.rb +18 -5
  78. data/lib/farce/local/vector.rb +3 -2
  79. data/lib/farce/memoize.rb +314 -0
  80. data/lib/farce/pool.rb +5 -0
  81. data/lib/farce/priority_queue.rb +1 -1
  82. data/lib/farce/publisher.rb +57 -0
  83. data/lib/farce/signal.rb +1 -1
  84. data/lib/farce/strict/publisher.rb +31 -0
  85. data/lib/farce/strict/vector.rb +3 -2
  86. data/lib/farce/subscription/definitions.rb +27 -0
  87. data/lib/farce/subscription.rb +121 -0
  88. data/lib/farce/transaction/map.rb +35 -1
  89. data/lib/farce/unshared/pool.rb +1 -1
  90. data/lib/farce/unshared/publisher.rb +25 -0
  91. data/lib/farce/unshared/scheduler.rb +3 -3
  92. data/lib/farce/unshared/vector.rb +3 -2
  93. data/lib/farce/vector.rb +12 -9
  94. data/lib/farce/version.rb +1 -1
  95. data/lib/farce/walker/definitions.rb +13 -2
  96. data/lib/farce/weak_atom.rb +5 -2
  97. data/lib/farce.rb +3 -1
  98. metadata +19 -2
@@ -169,6 +169,15 @@ module Farce
169
169
  super()
170
170
  end
171
171
 
172
+ # Pending-task capacity. Nil means the queue is unbounded.
173
+ def capacity = @queue.capacity
174
+
175
+ # Resize the pending queue without discarding tasks.
176
+ # JRuby and TruffleRuby cannot switch between bounded and unbounded queues.
177
+ def capacity=(value)
178
+ @queue.capacity = value.nil? ? nil : Integer(value)
179
+ end
180
+
172
181
  # @return [Exception, nil] The error encountered by the scheduler, if any.
173
182
  def error
174
183
  value = @error.value
@@ -305,7 +314,10 @@ module Farce
305
314
  # Otherwise, the block must be convertible to a Ractor-shareable proc.
306
315
  # Pass task data as arguments so the selected transfer mode can be applied to it.
307
316
  #
308
- # @overload schedule(*args, mode: :smart, auto_local: true)
317
+ # A `timeout` limits the combined wait for ownership and queue space.
318
+ # Expiry returns `nil` without enqueuing the task. Accepted tasks have no execution timeout.
319
+ #
320
+ # @overload schedule(*args, mode: :smart, auto_local: true, timeout: nil)
309
321
  # @!macro modes
310
322
  #
311
323
  # @param args [Array<Object>] Positional arguments passed to the task block.
@@ -313,8 +325,9 @@ module Farce
313
325
  # Argument transfer mode from {Farce::MODES}, including `:copy`, `:marshal`, and `:local`.
314
326
  # See {ModeManager#wrap}.
315
327
  # @param auto_local [Boolean] Whether to override mode with `:local` in the owning Ractor.
328
+ # @param timeout [Numeric, nil] Maximum scheduling wait in seconds. Nil waits indefinitely, zero polls.
316
329
  # @yield [*args] The task to execute.
317
- # @return [Scheduler] self.
330
+ # @return [self, nil] self when submitted, or nil when the scheduling timeout expires.
318
331
  # @raise [SchedulerClosedError] If the handle is closing, closed, or in an error state.
319
332
  # @see Ractor.shareable_proc
320
333
  def schedule(...) = enqueue_task(false, ...)
@@ -324,9 +337,13 @@ module Farce
324
337
  # it still waits for ownership to be assigned.
325
338
  #
326
339
  # @overload try_schedule(*args, mode: :smart, auto_local: true)
327
- # @param (see Farce::Abstract::Executor#schedule)
340
+ # @param args (see Farce::Abstract::Executor#schedule)
341
+ # @param mode (see Farce::Abstract::Executor#schedule)
342
+ # @param auto_local (see Farce::Abstract::Executor#schedule)
328
343
  # @return [self, nil] self when submitted, or nil when the queue is full.
329
- def try_schedule(...) = enqueue_task(true, ...)
344
+ def try_schedule(*, mode: :smart, auto_local: true, &)
345
+ enqueue_task(true, *, mode:, auto_local:, &)
346
+ end
330
347
 
331
348
  # (see Farce::Abstract::Executor#local?)
332
349
  def local?(wait: true)
@@ -418,13 +435,15 @@ module Farce
418
435
 
419
436
  private
420
437
 
421
- def enqueue_task(non_block, *args, mode: :smart, auto_local: true, &block)
438
+ def enqueue_task(non_block, *args, mode: :smart, auto_local: true, timeout: nil, &block)
422
439
  raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if closed?
423
440
 
424
441
  raise LocalJumpError, "Cannot schedule without a block" unless block
425
442
 
426
- task = prepare_task(args, block, mode, auto_local)
427
- return unless non_block ? @queue.try_push(task) : @queue.push(task)
443
+ deadline = scheduling_deadline(timeout)
444
+ task = prepare_task(args, block, mode, auto_local, timeout: Clock.remaining(deadline))
445
+ return unless task
446
+ return unless non_block ? @queue.try_push(task) : @queue.push(task, timeout: Clock.remaining(deadline))
428
447
  self
429
448
  rescue ClosedQueueError
430
449
  raise unless closed?
@@ -433,10 +452,11 @@ module Farce
433
452
 
434
453
  def new_queue(capacity:) = Internal::Queue.new(capacity:)
435
454
 
436
- def prepare_task(args, block, mode, auto_local)
455
+ def prepare_task(args, block, mode, auto_local, timeout: nil)
437
456
  if auto_local
438
- owner = @owner.wait_until_non_nil
457
+ owner = @owner.wait_until_non_nil(timeout:)
439
458
  raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if closed?
459
+ return unless owner
440
460
  mode = :local if owner == Ractor.current
441
461
  elsif mode == :local && (owner = @owner.value) && owner != Ractor.current
442
462
  raise Ractor::IsolationError, "cannot schedule local task outside the owning Ractor"
@@ -493,18 +513,25 @@ module Farce
493
513
  failure = nil
494
514
  @state.compare_and_set(:setup, :running)
495
515
  while keep_processing?
496
- break if @pool_worker && !@pool_worker.wait_for_admission(scheduler, runnable_count)
516
+ if @pool_worker
517
+ settings = @pool_worker.settings
518
+ settings = @pool_worker.wait_for_admission(scheduler, runnable_count, settings)
519
+ break unless settings
520
+ end
497
521
  timed_out = false
498
- timeout = @pool_worker&.idle_timeout
499
- task = @queue.pop(timeout:) { timed_out = true }
522
+ task = if @pool_worker
523
+ @queue.pop_interruptible(settings, settings.shrink_after) { timed_out = true }
524
+ else
525
+ @queue.pop { timed_out = true }
526
+ end
500
527
  if timed_out
501
- break if @pool_worker&.retire?
528
+ break if @pool_worker&.retire?(expected: settings)
502
529
  next
503
530
  end
504
531
  next unless task
505
532
  task = task.value if task.is_a?(Envelope)
506
533
  if task
507
- @pool_worker&.task_started
534
+ @pool_worker&.task_started(settings)
508
535
  yield task
509
536
  end
510
537
  end
@@ -156,18 +156,18 @@ module Farce
156
156
  timestamp = internal_queue.peek_priority
157
157
  wake_after = nil
158
158
  if timestamp
159
- delay = timestamp - Clock.now
159
+ delay = Clock.remaining(timestamp)
160
160
  return timestamp unless delay.positive?
161
161
  wake_after = delay if delay.finite?
162
162
  end
163
163
 
164
- remaining = remaining_timeout(deadline)
164
+ remaining = Clock.remaining(deadline)
165
165
 
166
166
  wait_for = wake_after && remaining ?
167
167
  (wake_after < remaining ? wake_after : remaining) :
168
168
  wake_after || remaining
169
169
  changed = internal_signal.wait(generation, timeout: wait_for)
170
- return UNDEFINED if !changed && deadline && remaining_timeout(deadline).zero?
170
+ return UNDEFINED if !changed && deadline && Clock.remaining(deadline).zero?
171
171
  end
172
172
  end
173
173
  end
@@ -1290,7 +1290,7 @@ module Farce
1290
1290
  # @param timeout [Numeric, nil] the total seconds available
1291
1291
  # @return [BasicObject, nil] the matching value, or nil on timeout
1292
1292
  # @raise [LocalJumpError] if no block is given
1293
- def wait_until(index, timeout: nil, &) = Internal.wait_until(self, index, timeout:, &)
1293
+ def wait_until(index, timeout: nil, &) = Internal.wait_until(internal_vector, index, timeout:, &)
1294
1294
 
1295
1295
  # Wait while the block returns a truthy value.
1296
1296
  # @yieldparam value [BasicObject, nil] the current value
@@ -1390,6 +1390,8 @@ module Farce
1390
1390
 
1391
1391
  private
1392
1392
 
1393
+ def normalize_vector_source(source) = source.is_a?(Integer) ? Array.new(source) : source
1394
+
1393
1395
  def mutate_vector(timeout: nil)
1394
1396
  result = nil
1395
1397
  completed = internal_vector.batch_update(timeout:) { |values| result = yield values }
data/lib/farce/atom.rb CHANGED
@@ -217,6 +217,13 @@ module Farce
217
217
  unwrap_value(result)
218
218
  end
219
219
 
220
+ # (see Abstract::Atom#wait_until)
221
+ def wait_until(timeout: nil)
222
+ raise LocalJumpError, "no block given" unless block_given?
223
+ result = Internal.wait_until(@atom, timeout:) { |current| yield(unwrap_value(current)) }
224
+ unwrap_value(result)
225
+ end
226
+
220
227
  # Wait until the current value no longer matches an expected value.
221
228
  # @param expected [BasicObject, nil] the value to compare with the current value
222
229
  # @param timeout [Numeric, nil] the maximum number of seconds to wait
@@ -227,11 +234,11 @@ module Farce
227
234
  deadline = Internal.timeout_deadline(timeout)
228
235
 
229
236
  while true
230
- current = @atom.get(timeout: Internal.remaining_timeout(deadline)) { TIMED_OUT }
237
+ current = @atom.get(timeout: Clock.remaining(deadline)) { TIMED_OUT }
231
238
  return unwrap_result(current, &) if TIMED_OUT.equal?(current)
232
239
  return unwrap_value(current) unless values_equal?(current, expected)
233
240
 
234
- result = @atom.wait_until_changed(current, timeout: Internal.remaining_timeout(deadline)) { TIMED_OUT }
241
+ result = @atom.wait_until_changed(current, timeout: Clock.remaining(deadline)) { TIMED_OUT }
235
242
  return unwrap_result(result, &) if TIMED_OUT.equal?(result)
236
243
  end
237
244
  end
data/lib/farce/clock.rb CHANGED
@@ -31,6 +31,18 @@ module Farce
31
31
  # @return [Float] the clock time in seconds
32
32
  def current = Process.clock_gettime(CLOCK_MONOTONIC) - MONOTONIC
33
33
 
34
+ # Returns the seconds left until a monotonic clock deadline.
35
+ # Expired deadlines return zero. Nil means there is no deadline and returns nil.
36
+ #
37
+ # @param deadline [Numeric, nil] a deadline from {#clock} or {#timeout}
38
+ # @raise [ArgumentError] if the deadline is NaN
39
+ # @return [Float, Integer, nil] the remaining seconds, zero, or nil
40
+ def remaining(deadline)
41
+ return if deadline.nil?
42
+ remaining = validate_timestamp(deadline - now)
43
+ remaining.positive? ? remaining : 0
44
+ end
45
+
34
46
  # Converts the given value to a monotonic clock time in seconds.
35
47
  #
36
48
  # @param value [nil, Numeric, Time, ActiveSupport::Duration, Hash]
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Concurrent
7
+ # A replacement for `concurrent-ruby`'s `CountDownLatch` implementation.
8
+ # @see https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/CountDownLatch.html
9
+ class CountDownLatch
10
+ include Shareable::Delegated
11
+ include Internal::Noncopyable
12
+
13
+ # @param count [Integer] the initial count for the latch.
14
+ # @raise [ArgumentError] if the count is not a non-negative integer.
15
+ def initialize(count = 1)
16
+ raise ArgumentError, "count must be a non-negative integer" unless count.is_a?(Integer) && count >= 0
17
+ @counter = Counter.new(count)
18
+ super()
19
+ end
20
+
21
+ # @!attribute [r] count
22
+ # @return [Integer] the current count of the latch.
23
+ def count = @counter.value
24
+
25
+ # Decrements the count of the latch by 1 if it is above 0.
26
+ # @return [void]
27
+ def count_down = @counter.decrement_if_above(0)
28
+
29
+ # Waits until the count reaches 0 or the optional timeout is reached.
30
+ # @param timeout [Numeric, nil] the maximum time to wait in seconds, or nil to wait indefinitely.
31
+ # @return [Boolean] true if the count reached 0, false if the timeout was reached.
32
+ def wait(timeout = nil) = !!@counter.wait_while_above(0, timeout:) # rubocop:disable Naming/PredicateMethod
33
+
34
+ private def freeze_backend = @counter
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Concurrent
7
+ # A replacement for `concurrent-ruby`'s `CyclicBarrier` implementation.
8
+ # @see https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/CyclicBarrier.html
9
+ class CyclicBarrier
10
+ include Shareable::Delegated
11
+ include Internal::Noncopyable
12
+
13
+ # @api private
14
+ class Generation
15
+ include Shareable::Delegated
16
+
17
+ attr_reader :waiting
18
+
19
+ def initialize
20
+ @waiting = Counter.new
21
+ @state = Strict::Atom.new(:waiting)
22
+ super
23
+ end
24
+
25
+ def wait(timeout) = @state.wait_until_changed(:waiting, timeout:)
26
+ def state = @state.value
27
+ def number_waiting = state == :waiting ? @waiting.value : 0
28
+
29
+ def state=(value)
30
+ @state.value = value
31
+ end
32
+
33
+ private def freeze_backend = @waiting
34
+ end
35
+ private_constant :Generation
36
+
37
+ # @return [Integer] the number of parties required to trip the barrier.
38
+ attr_reader :parties
39
+
40
+ # @param parties [Integer] the number of parties required to trip the barrier.
41
+ # @raise [ArgumentError] if parties is not a positive integer.
42
+ # @yield
43
+ # Optional block to trigger after the last waiter has arrived at the barrier but before they are released.
44
+ # Will be made Ractor-shareable. If it raises, the barrier breaks and the exception propagates to its caller.
45
+ def initialize(parties, &)
46
+ raise ArgumentError, "parties must be a positive integer" unless parties.is_a?(Integer) && parties.positive?
47
+ @parties = parties
48
+ @action = block_given? ? Ractor.shareable_proc(&) : nil
49
+ @generation = Strict::Atom.new(Generation.new)
50
+ super()
51
+ end
52
+
53
+ # @return [Boolean] whether the barrier is in a broken state.
54
+ def broken? = generation.state == :broken
55
+
56
+ # @return [Integer] the number of parties currently waiting at the barrier.
57
+ def number_waiting = generation.number_waiting
58
+
59
+ # Waits for all parties to reach the barrier. Successful generations can be reused immediately.
60
+ # A timeout breaks the barrier for all waiters until {#reset} is called.
61
+ # @param timeout [Numeric, nil] the maximum time to wait for the barrier.
62
+ # @return [Boolean] true if the barrier was successfully reached, false if it was broken by a timeout or reset.
63
+ def wait(timeout = nil) # rubocop:disable Naming/PredicateMethod
64
+ deadline = Internal.timeout_deadline(timeout)
65
+ waiting = nil
66
+ update do |current|
67
+ waiting = current
68
+ next current unless current.state == :waiting
69
+ current.waiting.increment
70
+ next current if current.waiting.value < @parties
71
+
72
+ completed = false
73
+ begin
74
+ @action&.call
75
+ completed = true
76
+ ensure
77
+ current.state = completed ? :fulfilled : :broken
78
+ end
79
+ Generation.new
80
+ end
81
+
82
+ unless waiting.wait(Clock.remaining(deadline))
83
+ update do |current|
84
+ # A trip or reset may have won the race with this waiter's timeout.
85
+ waiting.state = :broken if waiting.state == :waiting
86
+ current
87
+ end
88
+ end
89
+ waiting.state == :fulfilled
90
+ end
91
+
92
+ # Breaks the current generation and starts a new one. Existing waiters return false.
93
+ # @return [nil]
94
+ def reset
95
+ update do |current|
96
+ current.state = :broken
97
+ Generation.new
98
+ end
99
+ nil
100
+ end
101
+
102
+ private
103
+
104
+ def freeze_backend = @generation
105
+ def generation = @generation.value
106
+ def update(&) = @generation.update(&)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Concurrent
7
+ # A replacement for `concurrent-ruby`'s `Event` implementation.
8
+ # @see https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Event.html
9
+ class Event
10
+ include Shareable::Delegated
11
+ include Internal::Noncopyable
12
+
13
+ def initialize
14
+ # Odd generations are set. Even generations are reset without forgetting earlier signals.
15
+ @state = Strict::Atom.new(0)
16
+ super
17
+ end
18
+
19
+ # @return [Boolean] whether the event is currently set.
20
+ def set? = @state.value.odd?
21
+
22
+ # Sets the event.
23
+ # @return [true] always returns `true`.
24
+ def set # rubocop:disable Naming/PredicateMethod
25
+ try?
26
+ true
27
+ end
28
+
29
+ # @return [Boolean] `true` if the event was successfully set, `false` otherwise.
30
+ def try?
31
+ changed = false
32
+ @state.update do |current|
33
+ next current if current.odd?
34
+ changed = true
35
+ current + 1
36
+ end
37
+ changed
38
+ end
39
+
40
+ # @return [true] always returns `true`.
41
+ def reset # rubocop:disable Naming/PredicateMethod
42
+ @state.update { |current| current.odd? ? current + 1 : current }
43
+ true
44
+ end
45
+
46
+ # Waits for the event to be set or for the specified timeout to elapse.
47
+ # Resetting the event does not retract a signal already sent to current waiters.
48
+ # @param timeout [Numeric, nil] the maximum number of seconds to wait, or `nil` to wait indefinitely.
49
+ # @return [Boolean] `true` if the event was set, `false` if the timeout elapsed.
50
+ def wait(timeout = nil) # rubocop:disable Naming/PredicateMethod
51
+ deadline = Internal.timeout_deadline(timeout)
52
+ observed = @state.value
53
+ return true if observed.odd?
54
+ !!@state.wait_until_changed(observed, timeout: Clock.remaining(deadline))
55
+ end
56
+
57
+ private def freeze_backend = @state
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Concurrent
7
+ # A replacement for `concurrent-ruby`'s `Concurrent::FiberLocalVar`
8
+ # @see https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/FiberLocalVar.html
9
+ #
10
+ # May or may not be Ractor-shareable depending on provided default values.
11
+ #
12
+ # @attribute [rw] value
13
+ # @return [Object] the current value of the fiber-local variable
14
+ #
15
+ # @!method bind(value)
16
+ # @param value [Object] the value to bind to the fiber-local variable
17
+ # @yield the block with the value bound to the fiber-local variable
18
+ # @return [BasicObject] the result of the block
19
+ class FiberLocalVar < LocalVar
20
+ # @overload initialize(default = nil)
21
+ # @param default [Object] the default value for the fiber-local variable
22
+ #
23
+ # @overload initialize
24
+ # @yield the block to compute the default value for the fiber-local variable
25
+ def initialize(...) = super(:fiber, ...)
26
+ end
27
+
28
+ # Alias for `FiberLocalVar` to maintain compatibility with `LockLocalVar`
29
+ LockLocalVar = FiberLocalVar
30
+ end
31
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Concurrent
7
+ # @!visibility private
8
+ class LocalVar # :nodoc: all
9
+ include Abstract::Value
10
+ include Internal::Freeze::Tracked
11
+ include Internal::MarshalSupport::Reject
12
+
13
+ def initialize(scope, default = nil, &block)
14
+ @scope = scope
15
+
16
+ if block
17
+ @default_block = try_shareable(block)
18
+ @default = nil
19
+ else
20
+ @default_block = nil
21
+ @default = default
22
+ end
23
+
24
+ super()
25
+ __freeze__
26
+ end
27
+
28
+ def bind(value)
29
+ check_frozen!
30
+ return unless block_given?
31
+ current_storage = storage
32
+ old_value = self.value
33
+ current_storage[self] = value
34
+
35
+ begin
36
+ yield
37
+ ensure
38
+ current_storage[self] = old_value
39
+ end
40
+ end
41
+
42
+ def value = storage.store_if_absent(self) { @default_block ? @default_block.call : @default }
43
+
44
+ def value=(new_value)
45
+ check_frozen!
46
+ storage[self] = new_value
47
+ end
48
+
49
+ private
50
+
51
+ def storage = Internal::Storage.scope(@scope)
52
+
53
+ def try_shareable(block)
54
+ return block if Ractor.shareable?(block)
55
+ Ractor.shareable_proc(&block)
56
+ rescue StandardError
57
+ block
58
+ end
59
+
60
+ define_method(:__freeze__, Kernel.instance_method(:freeze))
61
+ end
62
+
63
+ private_constant :LocalVar
64
+ end
65
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Concurrent
7
+ # A replacement for `concurrent-ruby`'s `Concurrent::ThreadLocalVar`
8
+ # @see https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadLocalVar.html
9
+ #
10
+ # May or may not be Ractor-shareable depending on provided default values.
11
+ #
12
+ # @attribute [rw] value
13
+ # @return [Object] the current value of the thread-local variable
14
+ #
15
+ # @!method bind(value)
16
+ # @param value [Object] the value to bind to the thread-local variable
17
+ # @yield the block with the value bound to the thread-local variable
18
+ # @return [BasicObject] the result of the block
19
+ class ThreadLocalVar < LocalVar
20
+ # @overload initialize(default = nil)
21
+ # @param default [Object] the default value for the thread-local variable
22
+ #
23
+ # @overload initialize
24
+ # @yield the block to compute the default value for the thread-local variable
25
+ def initialize(...) = super(:thread, ...)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Concurrent
7
+ # A replacement for `Concurrent::Tuple` from `concurrent-ruby`.
8
+ # @see https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Tuple.html
9
+ class Tuple
10
+ include Enumerable
11
+ include Shareable::Delegated
12
+
13
+ # @return [Integer] the size of the tuple
14
+ attr_reader :size
15
+
16
+ # @param size [Integer] the size of the tuple
17
+ def initialize(size)
18
+ @size = Integer(size)
19
+ @tuple = Vector.new(@size)
20
+ super()
21
+ end
22
+
23
+ # Retrieves the value at the given index, or `nil` if the index is out of bounds.
24
+ # @param index [Integer] the index of the value to retrieve
25
+ # @return [BasicObject, nil] the value at the given index, or `nil` if out of bounds
26
+ def get(index) = index.between?(0, @size - 1) ? @tuple[index] : nil
27
+ alias volatile_get get
28
+
29
+ # Sets the value at the given index, or does nothing if the index is out of bounds.
30
+ # @param index [Integer] the index of the value to set
31
+ # @param value [BasicObject] the value to set
32
+ # @return [BasicObject, nil] the value set, or `nil` if out of bounds
33
+ def set(index, value) = index.between?(0, @size - 1) ? @tuple[index] = value : nil
34
+ alias volatile_set set
35
+
36
+ # Atomically sets the value at the given index to `new_value` if the current value is equal to `old_value`.
37
+ # @param index [Integer] the index of the value to set
38
+ # @param old_value [BasicObject] the expected current value
39
+ # @param new_value [BasicObject] the new value to set
40
+ # @return [Boolean] `true` if the value was set, `false` otherwise
41
+ def compare_and_set(index, old_value, new_value)
42
+ return false unless index.between?(0, @size - 1)
43
+ @tuple.compare_and_set(index, old_value, new_value)
44
+ end
45
+ alias cas compare_and_set
46
+
47
+ # Iterates over each element in the tuple.
48
+ # @yieldparam value [BasicObject] the value of each element
49
+ # @return [Enumerator, self] if no block is given, self otherwise
50
+ def each(&)
51
+ return enum_for(:each) unless block_given?
52
+ @tuple.each(&)
53
+ self
54
+ end
55
+
56
+ # @api private
57
+ def marshal_dump
58
+ [1, @tuple.to_a.map { Internal::MarshalSupport.value(it) }, frozen?]
59
+ end
60
+
61
+ # @api private
62
+ def marshal_load(data)
63
+ entries, frozen = Internal::MarshalSupport.payload(data, 2)
64
+ initialize(entries.length)
65
+ entries.each_with_index do |entry, index|
66
+ @tuple[index] = Internal::MarshalSupport.restore_value(entry)
67
+ end
68
+ Internal::MarshalSupport.freeze(self, frozen)
69
+ end
70
+
71
+ private def freeze_backend = @tuple
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ # Module to replace some functionality of `concurrent-ruby` with.
7
+ #
8
+ # Nested in a separate module so it can easily be moved into a dedicated gem
9
+ # (this would only happen on a major release).
10
+ #
11
+ # Not all of them are full drop-in replacements, especially when it comes to matching superclasses.
12
+ #
13
+ # If it matches the subset of functionality you need you could replace this:
14
+ #
15
+ # ```ruby
16
+ # require "concurrent"
17
+ # ```
18
+ #
19
+ # With this:
20
+ #
21
+ # ```ruby
22
+ # require "farce"
23
+ # Concurrent = Farce::Concurrent
24
+ # ```
25
+ #
26
+ # If developing a gem or just part of a larger app, you can and should do this under a namespace rather than globally.
27
+ #
28
+ # @see https://ruby-concurrency.github.io/concurrent-ruby/
29
+ module Concurrent
30
+ include Internal::Autoloads
31
+
32
+ Array = Vector
33
+ Atom = Farce::Atom
34
+ AtomicBoolean = Flag
35
+ AtomicFixnum = Counter
36
+ Exchanger = Farce::Exchanger
37
+ Hash = Farce::Map
38
+ Lock = Farce::Lock
39
+ Map = Farce::Map
40
+ ReadWriteLock = Farce::ReadWriteLock
41
+
42
+ autoload :LockLocalVar, "farce/concurrent/fiber_local_var"
43
+ private_constant :LocalVar
44
+
45
+ Farce.on_main do
46
+ Subscription.define(Event) { |_| subscriber.set }
47
+ Subscription.define(CountDownLatch) { |_| subscriber.count_down }
48
+ end
49
+ end
50
+ end