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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -2
- data/README.md +3 -1
- data/docs/benchmarks.md +37 -21
- data/docs/compatibility.md +13 -0
- data/docs/data-structures.md +37 -0
- data/docs/integrations.md +1 -0
- data/docs/introduction.md +19 -0
- data/docs/modes.md +2 -0
- data/docs/scheduling.md +35 -3
- data/docs/scopes.md +3 -1
- data/docs/shims.md +53 -0
- data/docs/utilities.md +31 -0
- data/docs/variants.md +9 -0
- data/lib/farce/abstract/concurrent_map.rb +122 -0
- data/lib/farce/abstract/counter.rb +35 -1
- data/lib/farce/abstract/executor.rb +30 -7
- data/lib/farce/abstract/flag.rb +12 -0
- data/lib/farce/abstract/pool.rb +158 -47
- data/lib/farce/abstract/priority_queue.rb +1 -1
- data/lib/farce/abstract/publisher.rb +189 -0
- data/lib/farce/abstract/queue.rb +29 -3
- data/lib/farce/abstract/scheduler.rb +41 -14
- data/lib/farce/abstract/timer_queue.rb +3 -3
- data/lib/farce/abstract/vector.rb +3 -1
- data/lib/farce/atom.rb +9 -2
- data/lib/farce/clock.rb +12 -0
- data/lib/farce/concurrent/count_down_latch.rb +37 -0
- data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
- data/lib/farce/concurrent/event.rb +60 -0
- data/lib/farce/concurrent/fiber_local_var.rb +31 -0
- data/lib/farce/concurrent/local_var.rb +65 -0
- data/lib/farce/concurrent/thread_local_var.rb +28 -0
- data/lib/farce/concurrent/tuple.rb +74 -0
- data/lib/farce/concurrent.rb +50 -0
- data/lib/farce/config.rb +112 -2
- data/lib/farce/counter.rb +0 -16
- data/lib/farce/engine/jruby/map.rb +8 -6
- data/lib/farce/engine/jruby/signal.rb +2 -2
- data/lib/farce/engine/jvm/farce.jar +0 -0
- data/lib/farce/engine/jvm/priority_queue.rb +14 -4
- data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
- data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
- data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
- data/lib/farce/engine/shared/exchanger.rb +2 -2
- data/lib/farce/engine/shared/queue.rb +43 -5
- data/lib/farce/engine/shared/ractor_selector.rb +1 -1
- data/lib/farce/engine/shared/strict_map.rb +5 -0
- data/lib/farce/engine/shared/unshared_atom.rb +1 -1
- data/lib/farce/engine/shared/unshared_vector.rb +8 -7
- data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
- data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
- data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
- data/lib/farce/engine/truffleruby/map.rb +8 -6
- data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
- data/lib/farce/engine/truffleruby/signal.rb +1 -1
- data/lib/farce/integrations/concurrent.rb +16 -3
- data/lib/farce/internal/blocking_priority_queue.rb +4 -9
- data/lib/farce/internal/flag_waiting.rb +1 -1
- data/lib/farce/internal/lease_initialization.rb +1 -1
- data/lib/farce/internal/lease_pool_state.rb +1 -1
- data/lib/farce/internal/lease_state.rb +2 -2
- data/lib/farce/internal/local_executor_methods.rb +3 -3
- data/lib/farce/internal/map_value_modes.rb +9 -2
- data/lib/farce/internal/pool_settings.rb +9 -0
- data/lib/farce/internal/pool_worker.rb +34 -21
- data/lib/farce/internal/reservation_waiting.rb +1 -1
- data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
- data/lib/farce/internal/weak_map_value_modes.rb +5 -0
- data/lib/farce/internal.rb +8 -8
- data/lib/farce/lazy.rb +3 -0
- data/lib/farce/lazy_ref.rb +3 -0
- data/lib/farce/local/lease.rb +2 -9
- data/lib/farce/local/lease_map.rb +2 -9
- data/lib/farce/local/pool.rb +38 -5
- data/lib/farce/local/publisher.rb +30 -0
- data/lib/farce/local/scheduler.rb +18 -5
- data/lib/farce/local/vector.rb +3 -2
- data/lib/farce/memoize.rb +314 -0
- data/lib/farce/pool.rb +5 -0
- data/lib/farce/priority_queue.rb +1 -1
- data/lib/farce/publisher.rb +57 -0
- data/lib/farce/signal.rb +1 -1
- data/lib/farce/strict/publisher.rb +31 -0
- data/lib/farce/strict/vector.rb +3 -2
- data/lib/farce/subscription/definitions.rb +27 -0
- data/lib/farce/subscription.rb +121 -0
- data/lib/farce/transaction/map.rb +35 -1
- data/lib/farce/unshared/pool.rb +1 -1
- data/lib/farce/unshared/publisher.rb +25 -0
- data/lib/farce/unshared/scheduler.rb +3 -3
- data/lib/farce/unshared/vector.rb +3 -2
- data/lib/farce/vector.rb +12 -9
- data/lib/farce/version.rb +1 -1
- data/lib/farce/walker/definitions.rb +13 -2
- data/lib/farce/weak_atom.rb +5 -2
- data/lib/farce.rb +3 -1
- 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
|
-
#
|
|
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 [
|
|
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(
|
|
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
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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 =
|
|
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 =
|
|
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 &&
|
|
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(
|
|
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:
|
|
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:
|
|
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
|