farce 0.4.1 → 0.5.0
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/ext/farce/farce.c +9 -0
- data/ext/farce/map.c +14 -3
- data/ext/farce/priority_queue.c +46 -1
- data/ext/farce/queue.c +141 -7
- data/ext/farce/vector.c +3 -3
- data/ext/farce/weak_map.c +12 -3
- data/ext/java/org/farce/PriorityQueue.java +18 -2
- 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 +18 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Abstract
|
|
7
|
+
# @abstract Shared superclass for all publisher implementations.
|
|
8
|
+
#
|
|
9
|
+
# Implements a publisher-subscriber (pub-sub) pattern.
|
|
10
|
+
# @see Farce::Publisher
|
|
11
|
+
class Publisher
|
|
12
|
+
include Internal::Freeze::Unfreezable
|
|
13
|
+
include Internal::Noncopyable
|
|
14
|
+
|
|
15
|
+
# @param subscribers [Array]
|
|
16
|
+
# the initial subscribers to add to the publisher, see {#subscribe}
|
|
17
|
+
def initialize(*subscribers)
|
|
18
|
+
@closed = Farce::Flag.new
|
|
19
|
+
super()
|
|
20
|
+
subscribers.each { subscribe(it) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Boolean] whether the publisher is closed.
|
|
24
|
+
def closed? = @closed.value
|
|
25
|
+
|
|
26
|
+
# Publishes the given payload to all current subscribers.
|
|
27
|
+
#
|
|
28
|
+
# Full bounded queues drop new messages.
|
|
29
|
+
# Failed publishing attempts to closed or frozen receivers cancel their subscriptions.
|
|
30
|
+
#
|
|
31
|
+
# @param payload [BasicObject] the payload to publish
|
|
32
|
+
# @return [self] the publisher itself
|
|
33
|
+
def publish(payload = nil)
|
|
34
|
+
raise "Cannot publish to a closed publisher" if closed?
|
|
35
|
+
map.each_value { it.publish(payload) }
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
alias send publish
|
|
39
|
+
alias << publish
|
|
40
|
+
alias broadcast publish
|
|
41
|
+
|
|
42
|
+
# @overload subscribe(subscriber)
|
|
43
|
+
# Adds a subscriber to the publisher.
|
|
44
|
+
# Register additional subscriber classes with {Subscription.define}.
|
|
45
|
+
# Publisher subscribers forward messages synchronously to their own subscribers.
|
|
46
|
+
# Avoid subscribing publishers in a cycle.
|
|
47
|
+
#
|
|
48
|
+
# @example Forward messages to another publisher
|
|
49
|
+
# upstream.subscribe(downstream)
|
|
50
|
+
# upstream.publish(:message) # also publishes to downstream's subscribers
|
|
51
|
+
#
|
|
52
|
+
# @param subscriber [BasicObject]
|
|
53
|
+
# The subscriber to add.
|
|
54
|
+
# Standard-library queues are supported by unshared and local publishers and retain the original payload.
|
|
55
|
+
# A full `Thread::SizedQueue` drops new messages. Publishing to a closed queue cancels its subscription.
|
|
56
|
+
#
|
|
57
|
+
# Calls one of the following methods:
|
|
58
|
+
# * {Ractor#send}
|
|
59
|
+
# * {Ractor::Port#send}
|
|
60
|
+
# * {Queue#try_push}
|
|
61
|
+
# * `Thread::Queue#push`
|
|
62
|
+
# * `Thread::SizedQueue#push` (nonblocking)
|
|
63
|
+
# * {Counter#increment} (without arguments)
|
|
64
|
+
# * {Flag#set}
|
|
65
|
+
# * {Atom#value=}
|
|
66
|
+
# * {Set#<<}
|
|
67
|
+
# * {Vector#<<}
|
|
68
|
+
# * {Signal#broadcast} (without arguments)
|
|
69
|
+
# * {Publisher#publish}
|
|
70
|
+
# * {Farce::Concurrent::Event#set} (without arguments)
|
|
71
|
+
# * {Farce::Concurrent::CountDownLatch#count_down} (without arguments)
|
|
72
|
+
# * `Proc#call`
|
|
73
|
+
#
|
|
74
|
+
# The concurrent-ruby integration supports these subscribers with unshared and local publishers:
|
|
75
|
+
# * `Concurrent::AtomicReference#value=`
|
|
76
|
+
# * `Concurrent::AtomicFixnum#increment` (without arguments)
|
|
77
|
+
# * `Concurrent::AtomicBoolean#make_true`
|
|
78
|
+
# * `Concurrent::Event#set` (without arguments)
|
|
79
|
+
# * `Concurrent::CountDownLatch#count_down` (without arguments)
|
|
80
|
+
#
|
|
81
|
+
# Set the default `mode`, `auto_local`, `capacity`, and similar options via initializers for these classes
|
|
82
|
+
# to influence how data transfer is handled.
|
|
83
|
+
#
|
|
84
|
+
# Support for additional subscribers can be added via {Subscription.define}.
|
|
85
|
+
#
|
|
86
|
+
# @overload subscribe(*args, mode: nil, scheduler: nil, capacity: nil)
|
|
87
|
+
# Adds a subscription callback.
|
|
88
|
+
# Messages run in order on the selected scheduler. A full queue drops new messages.
|
|
89
|
+
# Unsubscribing drains queued messages and stops the callback.
|
|
90
|
+
#
|
|
91
|
+
# @example
|
|
92
|
+
# counter = Farce::Counter.new
|
|
93
|
+
#
|
|
94
|
+
# publisher.subscribe(counter) do |counter, payload|
|
|
95
|
+
# counter.increment
|
|
96
|
+
# puts "Message ##{counter}: #{payload.inspect}"
|
|
97
|
+
# end
|
|
98
|
+
#
|
|
99
|
+
# @param args [Array] additional arguments to pass to the subscriber
|
|
100
|
+
# @param mode [Symbol, nil] The transfer mode to use
|
|
101
|
+
# @param scheduler [#schedule, nil] The scheduler to use for running the block
|
|
102
|
+
# @param capacity [Integer, nil] the maximum queued messages, or nil for an unbounded queue
|
|
103
|
+
# @yield [*args, payload] the block to be called with the given arguments and payload
|
|
104
|
+
# @yieldparam args [Array] the additional arguments passed to the block
|
|
105
|
+
# @yieldparam payload [Object] the payload sent via {#publish}
|
|
106
|
+
#
|
|
107
|
+
# @return [Subscription] the created subscription
|
|
108
|
+
def subscribe(...) = block_given? ? create_subscription(...) : add_subscription(...)
|
|
109
|
+
alias monitor subscribe
|
|
110
|
+
|
|
111
|
+
# @param subscriber [BasicObject] the subscriber to retrieve the subscription for
|
|
112
|
+
# @return [Subscription, nil] the subscription for the given subscriber, or nil if none exists.
|
|
113
|
+
def subscription_for(subscriber)
|
|
114
|
+
if subscriber.is_a?(Subscription)
|
|
115
|
+
return unless subscriber.publisher.equal?(self)
|
|
116
|
+
subscription = map[subscriber.key]
|
|
117
|
+
subscription if subscription.equal?(subscriber)
|
|
118
|
+
else
|
|
119
|
+
map[subscriber.__id__]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @return [Boolean] whether the given subscriber is currently subscribed.
|
|
124
|
+
def subscribed?(subscriber) = !!subscription_for(subscriber)&.subscribed?
|
|
125
|
+
|
|
126
|
+
# Unsubscribes the given subscriber from the publisher.
|
|
127
|
+
# @param subscriber [BasicObject] the subscriber to unsubscribe
|
|
128
|
+
# @return [void]
|
|
129
|
+
def unsubscribe(subscriber) = subscription_for(subscriber)&.unsubscribe
|
|
130
|
+
alias cancel unsubscribe
|
|
131
|
+
alias unmonitor unsubscribe
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def default_mode = :smart
|
|
136
|
+
def map = raise NoMethodError, "Subclasses must implement the private `map` method"
|
|
137
|
+
|
|
138
|
+
def add_subscription(subscriber)
|
|
139
|
+
if subscriber.is_a?(Subscription)
|
|
140
|
+
existing = subscription_for(subscriber)
|
|
141
|
+
return existing if existing
|
|
142
|
+
subscriber = subscriber.subscriber
|
|
143
|
+
end
|
|
144
|
+
return create_subscription(source: subscriber, &subscriber) if subscriber.is_a?(Proc)
|
|
145
|
+
|
|
146
|
+
map = self.map
|
|
147
|
+
map.store_if_absent(subscriber.__id__) { build_subscription(subscriber, map) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def create_subscription(*, capacity: nil, mode: nil, scheduler: nil, source: nil, &callback)
|
|
151
|
+
mode ||= default_mode
|
|
152
|
+
callback = Ractor.shareable_proc(&callback) unless mode == :local || Ractor.shareable?(callback)
|
|
153
|
+
scheduler ||= Farce
|
|
154
|
+
queue = Farce::Queue.new(capacity:, mode:)
|
|
155
|
+
map = self.map
|
|
156
|
+
created = false
|
|
157
|
+
subscription = map.store_if_absent((source || queue).__id__) do
|
|
158
|
+
created = true
|
|
159
|
+
build_subscription(queue, map, callback: true, source:)
|
|
160
|
+
end
|
|
161
|
+
return subscription unless created
|
|
162
|
+
|
|
163
|
+
scheduler.schedule(queue, callback, subscription, *, mode:) do |queue, callback, subscription, *args|
|
|
164
|
+
loop do
|
|
165
|
+
begin
|
|
166
|
+
payload = queue.pop
|
|
167
|
+
rescue ClosedQueueError
|
|
168
|
+
break
|
|
169
|
+
end
|
|
170
|
+
callback.call(*args, payload)
|
|
171
|
+
end
|
|
172
|
+
ensure
|
|
173
|
+
subscription.unsubscribe
|
|
174
|
+
queue.close
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
scheduled = true
|
|
178
|
+
subscription
|
|
179
|
+
ensure
|
|
180
|
+
unless scheduled
|
|
181
|
+
subscription&.unsubscribe if created
|
|
182
|
+
queue&.close
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def build_subscription(...) = Subscription.build(self, ...)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
data/lib/farce/abstract/queue.rb
CHANGED
|
@@ -11,8 +11,11 @@ module Farce
|
|
|
11
11
|
#
|
|
12
12
|
# Queues have a blocking and non-blocking interface.
|
|
13
13
|
#
|
|
14
|
-
# @!attribute [
|
|
15
|
-
#
|
|
14
|
+
# @!attribute [rw] capacity
|
|
15
|
+
# Assign a positive limit, or `nil` for an unbounded queue.
|
|
16
|
+
# Changes preserve queued values. Local queues change only the current scope.
|
|
17
|
+
# On JRuby and TruffleRuby, normal FIFO queues retain their initial bounded or unbounded choice.
|
|
18
|
+
# Switching between these states may raise `ArgumentError`. Priority and timer queues support both.
|
|
16
19
|
# @return [Integer, nil] The maximum number of items the queue can hold.
|
|
17
20
|
#
|
|
18
21
|
# @!attribute [r] num_waiting
|
|
@@ -173,10 +176,33 @@ module Farce
|
|
|
173
176
|
|
|
174
177
|
# Whether the queue is at capacity.
|
|
175
178
|
# @return [Boolean] `true` if the queue is at capacity, `false` otherwise.
|
|
176
|
-
def full?
|
|
179
|
+
def full?
|
|
180
|
+
limit = capacity
|
|
181
|
+
limit && size >= limit
|
|
182
|
+
end
|
|
177
183
|
|
|
178
184
|
def capacity = internal_queue.capacity
|
|
179
185
|
|
|
186
|
+
# Change the limit without discarding queued values. Returns immediately.
|
|
187
|
+
#
|
|
188
|
+
# New pushes wait while the queue is at or above the new limit.
|
|
189
|
+
# Increasing or removing the limit wakes blocked producers.
|
|
190
|
+
#
|
|
191
|
+
# Local queues change only the current scope. New scopes retain the constructor limit.
|
|
192
|
+
#
|
|
193
|
+
# Priority and timer queues support bounded and unbounded transitions on every engine.
|
|
194
|
+
#
|
|
195
|
+
# FIFO queues on JRuby and TruffleRuby retain their initial bounded or unbounded choice.
|
|
196
|
+
#
|
|
197
|
+
# @param value [Integer, nil] the positive capacity, or `nil` to remove the limit
|
|
198
|
+
# @return [Integer, nil] the new capacity
|
|
199
|
+
# @raise [ArgumentError] if the capacity is not positive
|
|
200
|
+
# @raise [ArgumentError] on JRuby and TruffleRuby, normal FIFO queues may reject transitions
|
|
201
|
+
# between bounded and unbounded states
|
|
202
|
+
def capacity=(value)
|
|
203
|
+
internal_queue.capacity = value.nil? ? nil : Integer(value)
|
|
204
|
+
end
|
|
205
|
+
|
|
180
206
|
def clear
|
|
181
207
|
internal_queue.clear
|
|
182
208
|
self
|
|
@@ -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
|