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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +91 -0
  3. data/README.md +117 -1451
  4. data/docs/compatibility.md +74 -0
  5. data/docs/concurrency.md +151 -0
  6. data/docs/data-structures.md +313 -0
  7. data/docs/installation.md +63 -0
  8. data/docs/integrations.md +124 -0
  9. data/docs/introduction.md +266 -0
  10. data/docs/modes.md +8 -5
  11. data/docs/scheduling.md +176 -0
  12. data/docs/scopes.md +3 -1
  13. data/docs/sharing.md +211 -0
  14. data/docs/shims.md +144 -0
  15. data/docs/utilities.md +93 -0
  16. data/docs/variants-and-scopes.md +92 -0
  17. data/docs/variants.md +18 -0
  18. data/lib/farce/abstract/executor.rb +130 -0
  19. data/lib/farce/abstract/flag.rb +6 -0
  20. data/lib/farce/abstract/pool.rb +334 -0
  21. data/lib/farce/abstract/scheduler.rb +539 -93
  22. data/lib/farce/backports.rb +274 -0
  23. data/lib/farce/engine/jruby/fiber_scheduler.rb +3 -0
  24. data/lib/farce/engine/jruby/scheduler_synchronization.rb +87 -0
  25. data/lib/farce/engine/jruby/signal.rb +7 -4
  26. data/lib/farce/engine/ruby/3.4/null_port.rb +21 -0
  27. data/lib/farce/engine/ruby/3.4/patch.rb +67 -0
  28. data/lib/farce/engine/ruby/3.4/port.rb +13 -12
  29. data/lib/farce/engine/ruby/3.4/ractor_methods.rb +0 -1
  30. data/lib/farce/engine/ruby/4.0/null_port.rb +36 -0
  31. data/lib/farce/engine/ruby/4.0/port.rb +1 -2
  32. data/lib/farce/engine/ruby/4.1/null_port.rb +5 -0
  33. data/lib/farce/engine/ruby.rb +7 -0
  34. data/lib/farce/engine/shared/main_scheduler.rb +2 -2
  35. data/lib/farce/engine/shared/null_port.rb +18 -0
  36. data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
  37. data/lib/farce/engine/shared/port.rb +41 -3
  38. data/lib/farce/engine/shared/proxy_owner.rb +1 -1
  39. data/lib/farce/engine/shared/queue.rb +4 -4
  40. data/lib/farce/engine/shared/ractor_methods.rb +2 -2
  41. data/lib/farce/engine/shared/ractor_selector.rb +73 -0
  42. data/lib/farce/engine/shared/unshared_atom.rb +9 -8
  43. data/lib/farce/engine/shared.rb +9 -0
  44. data/lib/farce/extension.rb +150 -0
  45. data/lib/farce/integrations/ractor_tmvar.rb +4 -1
  46. data/lib/farce/internal/fake_ractor.rb +10 -2
  47. data/lib/farce/internal/lease_initialization.rb +1 -1
  48. data/lib/farce/internal/local_executor_methods.rb +33 -0
  49. data/lib/farce/internal/local_scheduled_task.rb +27 -0
  50. data/lib/farce/internal/scheduled_task.rb +1 -1
  51. data/lib/farce/internal/scheduler_lifecycle.rb +7 -4
  52. data/lib/farce/internal/thread_pool_supervisor.rb +45 -0
  53. data/lib/farce/internal/unshared_pool_worker.rb +14 -0
  54. data/lib/farce/internal.rb +14 -0
  55. data/lib/farce/local/pool.rb +111 -0
  56. data/lib/farce/local/scheduler.rb +219 -0
  57. data/lib/farce/null_port.rb +12 -0
  58. data/lib/farce/pool.rb +2 -290
  59. data/lib/farce/proxy.rb +2 -2
  60. data/lib/farce/scheduler.rb +1 -502
  61. data/lib/farce/system.rb +10 -2
  62. data/lib/farce/task.rb +285 -0
  63. data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
  64. data/lib/farce/unshared/pool.rb +44 -0
  65. data/lib/farce/unshared/scheduler.rb +89 -0
  66. data/lib/farce/version.rb +1 -1
  67. data/lib/farce.rb +10 -18
  68. metadata +34 -2
@@ -4,6 +4,6 @@
4
4
 
5
5
  module Farce
6
6
  module Internal # :nodoc: all
7
- ParallelScheduler = ThreadScheduler.new(&Ractor.method(:new))
7
+ ParallelScheduler = ThreadExecutor.new(&Ractor.method(:new))
8
8
  end
9
9
  end
@@ -10,11 +10,19 @@ module Farce
10
10
  include MarshalSupport::Reject
11
11
  include Freeze::Unfreezable
12
12
 
13
- def initialize = @queue = Thread::Queue.new
14
- def closed? = @queue.closed?
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
- @queue.close
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
@@ -7,7 +7,7 @@ module Farce
7
7
  class ProxyOwner
8
8
  include ProxyOwnerNotifications
9
9
 
10
- class OwnerScheduler < Farce::ThreadScheduler
10
+ class OwnerScheduler < Farce::ThreadExecutor
11
11
  def initialize
12
12
  @threads = Thread::Queue.new
13
13
  super
@@ -12,10 +12,10 @@ module Farce
12
12
  attr_reader :capacity
13
13
 
14
14
  def initialize(capacity: 1024, track_age: false)
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
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
@@ -63,8 +63,8 @@ module Farce
63
63
  with_storage(name) { _1[_2] = value }
64
64
  end
65
65
 
66
- def select(...)
67
- raise "TODO: not implemented"
66
+ def select(*sources, timeout: nil)
67
+ RactorSelector.new.select(*sources, timeout:)
68
68
  end
69
69
 
70
70
  alias recv receive
@@ -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 = ConditionVariable.new
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
- return @signal.wait(@mutex) unless deadline
228
-
229
- remaining = deadline - Clock.now
230
- return false unless remaining.positive?
231
-
232
- @signal.wait(@mutex, remaining)
233
- true
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!
@@ -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 the attempt.
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 << state.status
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 << state.status
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 Lease construction without running user code under a storage lock.
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
- @mailbox << Completion.new(token, true, nil).freeze
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
- @mailbox << Completion.new(token, nil, exception).freeze
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
@@ -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|