farce 0.2.0-x86-linux-gnu → 0.4.0-x86-linux-gnu
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +91 -0
- data/README.md +117 -1451
- data/docs/compatibility.md +74 -0
- data/docs/concurrency.md +151 -0
- data/docs/data-structures.md +313 -0
- data/docs/installation.md +63 -0
- data/docs/integrations.md +124 -0
- data/docs/introduction.md +266 -0
- data/docs/modes.md +8 -5
- data/docs/scheduling.md +176 -0
- data/docs/scopes.md +3 -1
- data/docs/sharing.md +211 -0
- data/docs/shims.md +144 -0
- data/docs/utilities.md +93 -0
- data/docs/variants-and-scopes.md +92 -0
- data/docs/variants.md +18 -0
- data/lib/farce/abstract/executor.rb +130 -0
- data/lib/farce/abstract/flag.rb +6 -0
- data/lib/farce/abstract/pool.rb +334 -0
- data/lib/farce/abstract/scheduler.rb +539 -93
- data/lib/farce/backports.rb +274 -0
- data/lib/farce/engine/jruby/fiber_scheduler.rb +3 -0
- data/lib/farce/engine/jruby/scheduler_synchronization.rb +87 -0
- data/lib/farce/engine/jruby/signal.rb +7 -4
- data/lib/farce/engine/ruby/3.4/null_port.rb +21 -0
- data/lib/farce/engine/ruby/3.4/patch.rb +67 -0
- data/lib/farce/engine/ruby/3.4/port.rb +13 -12
- data/lib/farce/engine/ruby/3.4/ractor_methods.rb +0 -1
- data/lib/farce/engine/ruby/4.0/null_port.rb +36 -0
- data/lib/farce/engine/ruby/4.0/port.rb +1 -2
- data/lib/farce/engine/ruby/4.1/null_port.rb +5 -0
- data/lib/farce/engine/ruby.rb +7 -0
- data/lib/farce/engine/shared/main_scheduler.rb +2 -2
- data/lib/farce/engine/shared/null_port.rb +18 -0
- data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
- data/lib/farce/engine/shared/port.rb +41 -3
- data/lib/farce/engine/shared/proxy_owner.rb +1 -1
- data/lib/farce/engine/shared/queue.rb +4 -4
- data/lib/farce/engine/shared/ractor_methods.rb +2 -2
- data/lib/farce/engine/shared/ractor_selector.rb +73 -0
- data/lib/farce/engine/shared/unshared_atom.rb +9 -8
- data/lib/farce/engine/shared.rb +9 -0
- data/lib/farce/extension.rb +150 -0
- data/lib/farce/integrations/ractor_tmvar.rb +4 -1
- data/lib/farce/internal/fake_ractor.rb +10 -2
- data/lib/farce/internal/lease_initialization.rb +1 -1
- data/lib/farce/internal/local_executor_methods.rb +33 -0
- data/lib/farce/internal/local_scheduled_task.rb +27 -0
- data/lib/farce/internal/scheduled_task.rb +1 -1
- data/lib/farce/internal/scheduler_lifecycle.rb +7 -4
- data/lib/farce/internal/thread_pool_supervisor.rb +45 -0
- data/lib/farce/internal/unshared_pool_worker.rb +14 -0
- data/lib/farce/internal.rb +14 -0
- data/lib/farce/local/pool.rb +111 -0
- data/lib/farce/local/scheduler.rb +219 -0
- data/lib/farce/null_port.rb +12 -0
- data/lib/farce/pool.rb +2 -290
- data/lib/farce/proxy.rb +2 -2
- data/lib/farce/scheduler.rb +1 -502
- data/lib/farce/system.rb +10 -2
- data/lib/farce/task.rb +285 -0
- data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
- data/lib/farce/unshared/pool.rb +44 -0
- data/lib/farce/unshared/scheduler.rb +89 -0
- data/lib/farce/version.rb +1 -1
- data/lib/farce.rb +10 -18
- metadata +34 -2
|
@@ -10,11 +10,19 @@ module Farce
|
|
|
10
10
|
include MarshalSupport::Reject
|
|
11
11
|
include Freeze::Unfreezable
|
|
12
12
|
|
|
13
|
-
def initialize
|
|
14
|
-
|
|
13
|
+
def initialize
|
|
14
|
+
@queue = Thread::Queue.new
|
|
15
|
+
@select_mutex = Mutex.new
|
|
16
|
+
@select_waiters = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def closed? = @queue.closed?
|
|
15
20
|
|
|
16
21
|
def close
|
|
17
|
-
@
|
|
22
|
+
@select_mutex.synchronize do
|
|
23
|
+
@queue.close
|
|
24
|
+
@select_waiters.each(&:broadcast)
|
|
25
|
+
end
|
|
18
26
|
self
|
|
19
27
|
end
|
|
20
28
|
|
|
@@ -29,14 +37,44 @@ module Farce
|
|
|
29
37
|
raise Ractor::Error, "can not move #{value.class} object" unless value.is_a?(Unshareable::Movable)
|
|
30
38
|
end
|
|
31
39
|
@queue.push(value)
|
|
40
|
+
notify_selectors
|
|
32
41
|
self
|
|
33
42
|
rescue ::ClosedQueueError
|
|
34
43
|
raise Ractor::ClosedError, "The port was already closed"
|
|
35
44
|
end
|
|
36
45
|
|
|
46
|
+
# @api private
|
|
47
|
+
def register_selector(waiter)
|
|
48
|
+
@select_mutex.synchronize { @select_waiters << waiter }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @api private
|
|
52
|
+
def unregister_selector(waiter)
|
|
53
|
+
@select_mutex.synchronize { @select_waiters.delete(waiter) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @api private
|
|
57
|
+
def select_receive
|
|
58
|
+
@select_mutex.synchronize do
|
|
59
|
+
raise Ractor::ClosedError, "The port was already closed" if closed?
|
|
60
|
+
begin
|
|
61
|
+
value = @queue.pop(true)
|
|
62
|
+
rescue ThreadError
|
|
63
|
+
return
|
|
64
|
+
end
|
|
65
|
+
[self, value]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
37
69
|
alias << send
|
|
38
70
|
|
|
39
71
|
def inspect = "#<#{self.class.name} to:#1 id:#{object_id}>"
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def notify_selectors
|
|
76
|
+
@select_mutex.synchronize { @select_waiters.each(&:broadcast) }
|
|
77
|
+
end
|
|
40
78
|
end
|
|
41
79
|
|
|
42
80
|
BasePort = Port
|
|
@@ -12,10 +12,10 @@ module Farce
|
|
|
12
12
|
attr_reader :capacity
|
|
13
13
|
|
|
14
14
|
def initialize(capacity: 1024, track_age: false)
|
|
15
|
-
@queue
|
|
16
|
-
@capacity
|
|
17
|
-
@signal
|
|
18
|
-
@tracking
|
|
15
|
+
@queue = capacity ? Thread::SizedQueue.new(capacity) : Thread::Queue.new
|
|
16
|
+
@capacity = capacity
|
|
17
|
+
@signal = Signal.new
|
|
18
|
+
@tracking = track_age ? { lock: Mutex.new, timestamps: [], generation: 0 } : nil
|
|
19
19
|
@lifecycle = { lock: Mutex.new, hard_closed: false }
|
|
20
20
|
Freeze.publish(self)
|
|
21
21
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
# @!visibility private
|
|
7
|
+
module Internal # :nodoc: all
|
|
8
|
+
# Wait for shim port readiness without consuming messages from losing ports.
|
|
9
|
+
class RactorSelector
|
|
10
|
+
def select(*sources, timeout: nil)
|
|
11
|
+
raise ArgumentError, "specify at least one Ractor::Port or Ractor" if sources.empty?
|
|
12
|
+
sources.each do |source|
|
|
13
|
+
next if source.is_a?(Ractor) || source.is_a?(Port)
|
|
14
|
+
raise ArgumentError, "should be Ractor::Port or Ractor"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
timeout = duration(timeout)
|
|
18
|
+
deadline = Clock.now + timeout if timeout
|
|
19
|
+
monitors = {}.compare_by_identity
|
|
20
|
+
ports = sources.uniq.map do |source|
|
|
21
|
+
next source unless source.is_a?(Ractor)
|
|
22
|
+
|
|
23
|
+
port = BasePort.new
|
|
24
|
+
monitors[port] = source
|
|
25
|
+
source.monitor(port)
|
|
26
|
+
port
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
result = wait(ports, deadline)
|
|
30
|
+
return unless result
|
|
31
|
+
|
|
32
|
+
port, value = result
|
|
33
|
+
ractor = monitors[port]
|
|
34
|
+
ractor ? [ractor, ractor.value] : [port, value]
|
|
35
|
+
ensure
|
|
36
|
+
monitors&.each do |port, ractor|
|
|
37
|
+
ractor.unmonitor(port)
|
|
38
|
+
port.close
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def duration(value)
|
|
45
|
+
return if value.nil?
|
|
46
|
+
raise TypeError, "cannot convert String into time interval" if value.is_a?(String)
|
|
47
|
+
value = Float(value)
|
|
48
|
+
raise RangeError, "timeout out of range" unless value.finite?
|
|
49
|
+
raise ArgumentError, "time interval must not be negative" if value.negative?
|
|
50
|
+
value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def wait(ports, deadline)
|
|
54
|
+
wakeup = Signal.new
|
|
55
|
+
ports.each { it.register_selector(wakeup) }
|
|
56
|
+
while true
|
|
57
|
+
# Observe before checking readiness so a send during that check
|
|
58
|
+
# changes the generation and prevents the following wait from parking.
|
|
59
|
+
generation = wakeup.generation
|
|
60
|
+
ports.each do |port|
|
|
61
|
+
result = port.select_receive
|
|
62
|
+
return result if result
|
|
63
|
+
end
|
|
64
|
+
remaining = deadline - Clock.now if deadline
|
|
65
|
+
return if remaining && !remaining.positive?
|
|
66
|
+
return unless ReservationWaiting.wait(wakeup, generation, deadline)
|
|
67
|
+
end
|
|
68
|
+
ensure
|
|
69
|
+
ports.each { it.unregister_selector(wakeup) }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -17,7 +17,7 @@ module Farce
|
|
|
17
17
|
@value = value
|
|
18
18
|
@compare_by_identity = compare_by_identity
|
|
19
19
|
@mutex = Mutex.new
|
|
20
|
-
@signal =
|
|
20
|
+
@signal = Signal.new
|
|
21
21
|
@updating = false
|
|
22
22
|
@updating_fiber = nil
|
|
23
23
|
@updating_thread = nil
|
|
@@ -224,13 +224,14 @@ module Farce
|
|
|
224
224
|
end
|
|
225
225
|
|
|
226
226
|
def wait_for_signal(deadline)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
# Snapshot while holding the state lock so a change during registration is not missed.
|
|
228
|
+
generation = @signal.generation
|
|
229
|
+
begin
|
|
230
|
+
@mutex.unlock
|
|
231
|
+
ReservationWaiting.wait(@signal, generation, deadline)
|
|
232
|
+
ensure
|
|
233
|
+
@mutex.lock unless @mutex.owned?
|
|
234
|
+
end
|
|
234
235
|
end
|
|
235
236
|
|
|
236
237
|
def reject_update_wait!
|
data/lib/farce/engine/shared.rb
CHANGED
|
@@ -13,6 +13,15 @@ module Farce
|
|
|
13
13
|
module Internal # :nodoc: all
|
|
14
14
|
include Autoloads["#{__dir__}/shared"]
|
|
15
15
|
|
|
16
|
+
# Keep thread-only scheduler workers and their tasks in their emulated Ractor.
|
|
17
|
+
def with_scheduler_thread_group
|
|
18
|
+
group = ThreadGroup.new
|
|
19
|
+
FakeRactor::GROUPS[group] = Ractor.current
|
|
20
|
+
yield group
|
|
21
|
+
ensure
|
|
22
|
+
FakeRactor::GROUPS.delete(group)
|
|
23
|
+
end
|
|
24
|
+
|
|
16
25
|
# @api private
|
|
17
26
|
# The shim's shareable? can delegate to a lazy or weak value and change its state.
|
|
18
27
|
# Snapshot metadata uses only structural properties on engines without Ractors.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
# Defines a reusable extension for adding methods to target classes.
|
|
7
|
+
# These methods can either be activated as refinements, or installed globally to extend the target classes.
|
|
8
|
+
#
|
|
9
|
+
# ```ruby
|
|
10
|
+
# DoubleSize = Farce::Extension.define(String) do
|
|
11
|
+
# def double_size = size * 2
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# module Namespace
|
|
15
|
+
# using DoubleSize
|
|
16
|
+
# "hello".double_size # => 10
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# "hello".respond_to?(:double_size) # => false
|
|
20
|
+
#
|
|
21
|
+
# DoubleSize.install
|
|
22
|
+
# "hello".respond_to?(:double_size) # => true
|
|
23
|
+
# ```
|
|
24
|
+
#
|
|
25
|
+
# ## Extending multiple classes
|
|
26
|
+
#
|
|
27
|
+
# An extension can also modify multiple target classes simultaneously:
|
|
28
|
+
#
|
|
29
|
+
# ```ruby
|
|
30
|
+
# DoubleSize = Farce::Extension.new do
|
|
31
|
+
# define(String) { def double_size = size * 2 }
|
|
32
|
+
# define(Array) { def double_size = sum(&:double_size) }
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# using DoubleSize
|
|
36
|
+
# ["hello", "foo"].double_size # => 16
|
|
37
|
+
# ```
|
|
38
|
+
class Extension < Module
|
|
39
|
+
# @api private
|
|
40
|
+
class Mixin < Module # :nodoc:
|
|
41
|
+
attr_reader :extension, :target
|
|
42
|
+
|
|
43
|
+
def initialize(extension, target, &)
|
|
44
|
+
@extension = extension
|
|
45
|
+
@target = target
|
|
46
|
+
super(&)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def name = -"#{@extension.name}[#{@target.inspect}]"
|
|
50
|
+
|
|
51
|
+
def install
|
|
52
|
+
return if @target.ancestors.include?(self)
|
|
53
|
+
@target.prepend(self)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
private_constant :Mixin
|
|
57
|
+
|
|
58
|
+
# @overload define(*targets)
|
|
59
|
+
# Defines an extension for the given target classes.
|
|
60
|
+
# @param targets [Array<Class>] the target classes to define methods for
|
|
61
|
+
# @yield the block containing method definitions
|
|
62
|
+
# @yieldreceiver [Module, Refinement] the module/refinement used for extending the target class.
|
|
63
|
+
# @return [Extension] the newly created extension instance
|
|
64
|
+
# @see Extension#define
|
|
65
|
+
def self.define(...) = new { define(...) }
|
|
66
|
+
|
|
67
|
+
# Creates a new extension.
|
|
68
|
+
# @yield Optional block for `define` calls or adding methods to the extension itself.
|
|
69
|
+
# @yieldreceiver [Extension] the extension instance itself.
|
|
70
|
+
def initialize
|
|
71
|
+
check_state!
|
|
72
|
+
@definitions = Internal::StrictMap.new
|
|
73
|
+
@lock = Lock.new
|
|
74
|
+
@installed = Flag.new
|
|
75
|
+
super
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @api private
|
|
79
|
+
def [](target) = @definitions[target]
|
|
80
|
+
|
|
81
|
+
# Defines additional methods for a given target class.
|
|
82
|
+
#
|
|
83
|
+
# The given block will be invoked at least twice. Once in the context of a refinement for the target class,
|
|
84
|
+
# and once in the context of the mixin module that will be prepended to the target class.
|
|
85
|
+
#
|
|
86
|
+
# @example
|
|
87
|
+
# extension = Farce::Extension.new
|
|
88
|
+
# extension.define(String) { def double_size = size * 2 }
|
|
89
|
+
#
|
|
90
|
+
# module Namespace
|
|
91
|
+
# using extension
|
|
92
|
+
# String.method_defined?(:double_size) # => true
|
|
93
|
+
# end
|
|
94
|
+
#
|
|
95
|
+
# String.method_defined?(:double_size) # => false
|
|
96
|
+
# extension.install
|
|
97
|
+
# String.method_defined?(:double_size) # => true
|
|
98
|
+
#
|
|
99
|
+
# @param targets [Array<Class>] the target classes to define methods for
|
|
100
|
+
# @yield the block containing method definitions
|
|
101
|
+
# @yieldreceiver [Module, Refinement] the module/refinement used for extending the target class.
|
|
102
|
+
# @return [self] the extension instance
|
|
103
|
+
def define(*targets, &)
|
|
104
|
+
@lock.synchronize do
|
|
105
|
+
check_state!
|
|
106
|
+
targets.each do |target|
|
|
107
|
+
refine(target, &)
|
|
108
|
+
mixin = @definitions.store_if_absent(target) { Mixin.new(self, target) }
|
|
109
|
+
mixin.module_eval(&)
|
|
110
|
+
mixin.install if installed?
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Installs the extension globally.
|
|
117
|
+
# @return [void]
|
|
118
|
+
def install
|
|
119
|
+
@lock.synchronize do
|
|
120
|
+
return false if installed?
|
|
121
|
+
check_state!(allow_frozen: true)
|
|
122
|
+
@definitions.each_value(&:install)
|
|
123
|
+
yield if block_given?
|
|
124
|
+
@installed.set
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @return [Boolean] whether the extension has been installed
|
|
129
|
+
def installed? = @installed.set?
|
|
130
|
+
|
|
131
|
+
# Freezes the extension.
|
|
132
|
+
#
|
|
133
|
+
# No {#define definitions} can be added after freezing the extension.
|
|
134
|
+
# @return [self] the frozen extension
|
|
135
|
+
def freeze
|
|
136
|
+
return self if frozen?
|
|
137
|
+
@lock.synchronize do
|
|
138
|
+
@definitions.freeze
|
|
139
|
+
super
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def check_state!(allow_frozen: false)
|
|
146
|
+
raise Ractor::IsolationError, "must be called from main Ractor" if Internal.native_ractors? && !Ractor.main?
|
|
147
|
+
Internal::Freeze.check(self) unless allow_frozen
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -13,7 +13,10 @@ module Farce
|
|
|
13
13
|
# Take, put, and swap operate on the value staged in this attempt.
|
|
14
14
|
# An empty read, take, or swap, or a put into a full TMVar fails the
|
|
15
15
|
# attempt. Farce.transaction retries these conditions within its retry limit.
|
|
16
|
-
# The try_* methods return nil or false without failing
|
|
16
|
+
# The try_* methods return nil or false when unavailable without failing
|
|
17
|
+
# the attempt. Blocks passed to try_read and try_take run only when full,
|
|
18
|
+
# including for stored nil, and return the block's result.
|
|
19
|
+
# Create an empty TMVar with Ractor::TMVar.new and a full nil with .new(nil).
|
|
17
20
|
# Assigned values are made shareable, matching Ractor::TVar's setter.
|
|
18
21
|
#
|
|
19
22
|
# Run outside Ractor.atomically, as required by {RactorTVar}.
|
|
@@ -21,6 +21,7 @@ module Farce
|
|
|
21
21
|
INSTANCE = Freeze.publish(new)
|
|
22
22
|
DEFAULT_PORT = BasePort.new
|
|
23
23
|
|
|
24
|
+
def alive? = true
|
|
24
25
|
def default_port = DEFAULT_PORT
|
|
25
26
|
def inspect = "#<Farce::Ractor#1 running>"
|
|
26
27
|
def join = sleep
|
|
@@ -78,7 +79,7 @@ module Farce
|
|
|
78
79
|
ensure
|
|
79
80
|
state.mutex.synchronize do
|
|
80
81
|
state.monitors.each do |monitor|
|
|
81
|
-
monitor
|
|
82
|
+
FakeRactor.send_status(monitor, self, state.status)
|
|
82
83
|
end
|
|
83
84
|
state.monitors = nil
|
|
84
85
|
end
|
|
@@ -97,6 +98,8 @@ module Farce
|
|
|
97
98
|
Farce::Ractor[key] = value
|
|
98
99
|
end
|
|
99
100
|
|
|
101
|
+
def alive? = STATES[self].status == :running
|
|
102
|
+
|
|
100
103
|
def monitor(port) # rubocop:disable Naming/PredicateMethod
|
|
101
104
|
state = STATES[self]
|
|
102
105
|
|
|
@@ -108,7 +111,7 @@ module Farce
|
|
|
108
111
|
end
|
|
109
112
|
end
|
|
110
113
|
|
|
111
|
-
port
|
|
114
|
+
FakeRactor.send_status(port, self, state.status)
|
|
112
115
|
false
|
|
113
116
|
end
|
|
114
117
|
|
|
@@ -148,6 +151,11 @@ module Farce
|
|
|
148
151
|
undef clone
|
|
149
152
|
end
|
|
150
153
|
|
|
154
|
+
def self.send_status(port, ractor, status)
|
|
155
|
+
payload = System.ruby_version >= "4.1" ? [ractor, status].freeze : status
|
|
156
|
+
port << payload
|
|
157
|
+
end
|
|
158
|
+
|
|
151
159
|
def self.current
|
|
152
160
|
# JRuby fibers expose a backing thread with a different thread group.
|
|
153
161
|
thread = Internal.storage_thread(Thread.current)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
module Farce
|
|
6
6
|
# @!visibility private
|
|
7
7
|
module Internal # :nodoc: all
|
|
8
|
-
# Coordinates lazy
|
|
8
|
+
# Coordinates lazy construction without running user code under a storage lock.
|
|
9
9
|
class LeaseInitialization
|
|
10
10
|
def initialize
|
|
11
11
|
@mutex = Mutex.new
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Internal # :nodoc: all
|
|
7
|
+
# The local scheduling and execution interface for Ractor-local executors.
|
|
8
|
+
module LocalExecutorMethods
|
|
9
|
+
LOCAL_MODES = %i[local smart smart_raise smart_move smart_copy].freeze
|
|
10
|
+
private_constant :LOCAL_MODES
|
|
11
|
+
|
|
12
|
+
# @param wait [Boolean] Accepted for compatibility with {Abstract::Executor#local?}.
|
|
13
|
+
# @return [true] Workers run in the calling Ractor.
|
|
14
|
+
def local?(wait: true) = true # rubocop:disable Lint/UnusedMethodArgument
|
|
15
|
+
|
|
16
|
+
# Run a task directly, preserving its block, arguments, and calling fiber.
|
|
17
|
+
# `mode` and `auto_local` follow the same validation as {#schedule}.
|
|
18
|
+
# @param (see Abstract::Executor#execute)
|
|
19
|
+
# @return [self]
|
|
20
|
+
def execute(*, mode: :smart, auto_local: true, &)
|
|
21
|
+
validate_local_mode(mode, auto_local)
|
|
22
|
+
super(*, mode: :local, auto_local: true, &)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def validate_local_mode(mode, auto_local)
|
|
28
|
+
return if auto_local || LOCAL_MODES.include?(mode)
|
|
29
|
+
raise ArgumentError, "only local or smart modes are supported"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Internal # :nodoc: all
|
|
7
|
+
# A Ractor-local task that retains its block and argument references.
|
|
8
|
+
class LocalScheduledTask
|
|
9
|
+
include Noncopyable
|
|
10
|
+
include Farce::Unshareable
|
|
11
|
+
|
|
12
|
+
def initialize(args, block)
|
|
13
|
+
@args = args
|
|
14
|
+
@block = block
|
|
15
|
+
super()
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_proc(completion = nil)
|
|
19
|
+
lambda do
|
|
20
|
+
@block.call(*@args)
|
|
21
|
+
ensure
|
|
22
|
+
completion&.task_finished
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -12,7 +12,7 @@ module Farce
|
|
|
12
12
|
MANAGER = ModeManager.new
|
|
13
13
|
|
|
14
14
|
def initialize(args, block, mode)
|
|
15
|
-
@block = Ractor.shareable_proc(&block)
|
|
15
|
+
@block = Ractor.shareable?(block) ? block : Ractor.shareable_proc(&block)
|
|
16
16
|
@unwrap = false
|
|
17
17
|
@args = args.map! do |arg|
|
|
18
18
|
wrapped = MANAGER.wrap(arg, mode:)
|
|
@@ -82,15 +82,13 @@ module Farce
|
|
|
82
82
|
|
|
83
83
|
def unblock(_blocker, fiber) # rubocop:disable Naming/PredicateMethod
|
|
84
84
|
return false if @closed || !(token = current_wait(fiber))
|
|
85
|
-
|
|
86
|
-
wakeup
|
|
85
|
+
complete_wait(token, true)
|
|
87
86
|
true
|
|
88
87
|
end
|
|
89
88
|
|
|
90
89
|
def fiber_interrupt(fiber, exception) # rubocop:disable Naming/PredicateMethod
|
|
91
90
|
return false if @closed || !(token = current_wait(fiber))
|
|
92
|
-
|
|
93
|
-
wakeup
|
|
91
|
+
complete_wait(token, nil, exception)
|
|
94
92
|
true
|
|
95
93
|
end
|
|
96
94
|
|
|
@@ -202,6 +200,11 @@ module Farce
|
|
|
202
200
|
|
|
203
201
|
private
|
|
204
202
|
|
|
203
|
+
def complete_wait(token, value, error = nil)
|
|
204
|
+
@mailbox << Completion.new(token, value, error).freeze
|
|
205
|
+
wakeup
|
|
206
|
+
end
|
|
207
|
+
|
|
205
208
|
def finish_close
|
|
206
209
|
return if @closed
|
|
207
210
|
shutdown
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Internal # :nodoc: all
|
|
7
|
+
# A Ractor-local service for delayed pool growth and executor shutdown requests.
|
|
8
|
+
class ThreadPoolSupervisor
|
|
9
|
+
include Farce::Unshareable
|
|
10
|
+
|
|
11
|
+
def self.current = Storage.store_if_absent(self) { new }
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@queue = TimerQueue.new(mode: :raise)
|
|
15
|
+
@closed = Atom.new(false)
|
|
16
|
+
@worker = Thread.new do
|
|
17
|
+
loop do
|
|
18
|
+
entry = @queue.pop
|
|
19
|
+
break if entry == :stop
|
|
20
|
+
pool, token = entry
|
|
21
|
+
pool = pool.value
|
|
22
|
+
token == :close ? pool.close : pool.scale_if_needed(token)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def close_handle(pool) = [@queue, Envelope::Local.new(pool)].freeze
|
|
29
|
+
|
|
30
|
+
def schedule(pool, token, delay)
|
|
31
|
+
raise ::Farce::ClosedQueueError, "pool supervisor is closed" if @closed.value
|
|
32
|
+
@queue.push([Envelope::Local.new(pool), token].freeze, delay:)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def close
|
|
36
|
+
return unless @closed.compare_and_set(false, true)
|
|
37
|
+
@queue.push(:stop, clock: 0)
|
|
38
|
+
@worker.join
|
|
39
|
+
@queue.close
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
at_exit { Storage[ThreadPoolSupervisor]&.close }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Internal # :nodoc: all
|
|
7
|
+
# A worker coordinator pinned to its pool's Ractor.
|
|
8
|
+
class UnsharedPoolWorker < PoolWorker
|
|
9
|
+
include Farce::Unshareable
|
|
10
|
+
|
|
11
|
+
private def publish_shareable = pin_unshareable
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/farce/internal.rb
CHANGED
|
@@ -54,6 +54,19 @@ module Farce
|
|
|
54
54
|
new_self
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# @api private
|
|
58
|
+
# @return [Abstract::Executor, Fiber::Scheduler, nil]
|
|
59
|
+
def local_scheduler(create: false)
|
|
60
|
+
if Fiber.respond_to?(:scheduler) && fiber_scheduler = Fiber.scheduler
|
|
61
|
+
return fiber_scheduler
|
|
62
|
+
end
|
|
63
|
+
return Internal::MainScheduler if Ractor.main?
|
|
64
|
+
return Internal::Storage[:local_scheduler] unless create
|
|
65
|
+
Internal::Storage.store_if_absent(:local_scheduler) do
|
|
66
|
+
Internal.native_ractors? ? Scheduler.create(Thread) : ThreadExecutor.new
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
57
70
|
def timeout_deadline(timeout)
|
|
58
71
|
return if timeout.nil?
|
|
59
72
|
|
|
@@ -101,6 +114,7 @@ module Farce
|
|
|
101
114
|
def prepare_mutable_numeric(_) = nil
|
|
102
115
|
def prepare_map_access(_klass, _kind) = nil
|
|
103
116
|
def finalize_engine = nil
|
|
117
|
+
def with_scheduler_thread_group = yield(ThreadGroup.new)
|
|
104
118
|
|
|
105
119
|
def delegate(from, to, *methods)
|
|
106
120
|
methods.each do |method|
|