farce 0.1.0-aarch64-linux-gnu → 0.3.0-aarch64-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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -1
  3. data/README.md +111 -17
  4. data/docs/gems/dry-types.md +8 -8
  5. data/docs/modes.md +75 -15
  6. data/docs/scopes.md +3 -1
  7. data/docs/variants.md +18 -0
  8. data/ext/farce/atom.c +37 -22
  9. data/ext/farce/bounded_map.c +17 -13
  10. data/ext/farce/containers.h +15 -0
  11. data/ext/farce/counter.c +13 -18
  12. data/ext/farce/exchanger.c +5 -1
  13. data/ext/farce/farce.c +1 -1
  14. data/ext/farce/flag.c +39 -3
  15. data/ext/farce/io.c +3 -3
  16. data/ext/farce/lock.c +17 -23
  17. data/ext/farce/map.c +88 -66
  18. data/ext/farce/priority_queue.c +55 -43
  19. data/ext/farce/queue.c +10 -4
  20. data/ext/farce/reactor.c +52 -25
  21. data/ext/farce/reactor.h +1 -1
  22. data/ext/farce/signal.c +5 -1
  23. data/ext/farce/transaction.c +20 -24
  24. data/ext/farce/tree_map.c +33 -18
  25. data/ext/farce/trie.c +81 -43
  26. data/ext/farce/unshareable.c +168 -19
  27. data/ext/farce/unshared_io_pool.h +21 -12
  28. data/ext/farce/unshared_signal.c +13 -2
  29. data/ext/farce/unshared_wait.h +3 -0
  30. data/ext/farce/vector.c +82 -18
  31. data/ext/farce/weak_map.c +75 -63
  32. data/lib/farce/_yard/macros.rb +18 -3
  33. data/lib/farce/abstract/executor.rb +130 -0
  34. data/lib/farce/abstract/flag.rb +73 -0
  35. data/lib/farce/abstract/pool.rb +334 -0
  36. data/lib/farce/abstract/queue.rb +4 -3
  37. data/lib/farce/abstract/scheduler.rb +539 -93
  38. data/lib/farce/abstract/set.rb +17 -5
  39. data/lib/farce/abstract/sorted_set.rb +2 -2
  40. data/lib/farce/abstract/timer_queue.rb +49 -10
  41. data/lib/farce/abstract/vector.rb +510 -28
  42. data/lib/farce/atom.rb +1 -1
  43. data/lib/farce/class_mirror.rb +20 -7
  44. data/lib/farce/config.rb +7 -1
  45. data/lib/farce/engine/jruby/signal.rb +7 -4
  46. data/lib/farce/engine/jvm/flag.rb +15 -3
  47. data/lib/farce/engine/ruby/3.4/farce.so +0 -0
  48. data/lib/farce/engine/ruby/4.0/farce.so +0 -0
  49. data/lib/farce/engine/ruby/shared/require_hook.rb +18 -0
  50. data/lib/farce/engine/ruby.rb +7 -0
  51. data/lib/farce/engine/shared/main_scheduler.rb +2 -2
  52. data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
  53. data/lib/farce/engine/shared/proxy_owner.rb +1 -1
  54. data/lib/farce/engine/shared/unshared_atom.rb +9 -8
  55. data/lib/farce/engine/shared/unshared_vector.rb +31 -4
  56. data/lib/farce/engine/shared/vector.rb +8 -0
  57. data/lib/farce/engine/shared/weak_map/index.rb +14 -1
  58. data/lib/farce/engine/shared.rb +9 -0
  59. data/lib/farce/engine/truffleruby/native/flag.rb +15 -3
  60. data/lib/farce/envelope.rb +54 -7
  61. data/lib/farce/exchanger.rb +1 -1
  62. data/lib/farce/flag.rb +2 -0
  63. data/lib/farce/integrations/active_support.rb +1 -1
  64. data/lib/farce/integrations/bson.rb +1 -1
  65. data/lib/farce/integrations/cbor.rb +1 -1
  66. data/lib/farce/integrations/concurrent.rb +3 -3
  67. data/lib/farce/integrations/date.rb +8 -0
  68. data/lib/farce/integrations/dry_types.rb +7 -4
  69. data/lib/farce/integrations/json.rb +2 -2
  70. data/lib/farce/integrations/msgpack.rb +1 -1
  71. data/lib/farce/integrations/oj.rb +3 -4
  72. data/lib/farce/integrations/psych.rb +1 -1
  73. data/lib/farce/integrations/ractor_sharing.rb +1 -1
  74. data/lib/farce/integrations/sorted_set.rb +1 -1
  75. data/lib/farce/integrations/weakref.rb +9 -1
  76. data/lib/farce/integrations/yajl.rb +3 -3
  77. data/lib/farce/integrations.rb +33 -23
  78. data/lib/farce/internal/flag_waiting.rb +30 -0
  79. data/lib/farce/internal/freeze.rb +8 -3
  80. data/lib/farce/internal/lease_initialization.rb +1 -1
  81. data/lib/farce/internal/local_executor_methods.rb +33 -0
  82. data/lib/farce/internal/local_scheduled_task.rb +27 -0
  83. data/lib/farce/internal/map_value_modes.rb +1 -1
  84. data/lib/farce/internal/scheduled_task.rb +1 -1
  85. data/lib/farce/internal/scheduler_lifecycle.rb +1 -1
  86. data/lib/farce/internal/thread_pool_supervisor.rb +45 -0
  87. data/lib/farce/internal/unshared_pool_worker.rb +14 -0
  88. data/lib/farce/internal.rb +14 -0
  89. data/lib/farce/lazy.rb +4 -4
  90. data/lib/farce/lazy_ref.rb +3 -3
  91. data/lib/farce/lfu_map.rb +1 -1
  92. data/lib/farce/local/flag.rb +4 -0
  93. data/lib/farce/local/pool.rb +111 -0
  94. data/lib/farce/local/scheduler.rb +219 -0
  95. data/lib/farce/local/scoped.rb +1 -1
  96. data/lib/farce/lru_map.rb +1 -1
  97. data/lib/farce/map.rb +2 -2
  98. data/lib/farce/mode_manager/smart_rule.rb +138 -0
  99. data/lib/farce/mode_manager.rb +12 -3
  100. data/lib/farce/molecule.rb +2 -2
  101. data/lib/farce/pool.rb +2 -290
  102. data/lib/farce/port.rb +4 -4
  103. data/lib/farce/priority_queue.rb +1 -1
  104. data/lib/farce/proxy/register.rb +6 -0
  105. data/lib/farce/proxy/supervisor.rb +1 -1
  106. data/lib/farce/proxy.rb +9 -4
  107. data/lib/farce/queue.rb +1 -1
  108. data/lib/farce/scheduler.rb +1 -502
  109. data/lib/farce/set.rb +1 -1
  110. data/lib/farce/sorted_set.rb +1 -1
  111. data/lib/farce/task.rb +285 -0
  112. data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
  113. data/lib/farce/timer_queue.rb +20 -6
  114. data/lib/farce/transaction/vector.rb +43 -9
  115. data/lib/farce/transaction/wrapper.rb +14 -0
  116. data/lib/farce/tree_map.rb +1 -1
  117. data/lib/farce/unsafe/lfu_map.rb +2 -0
  118. data/lib/farce/unsafe/lru_map.rb +2 -0
  119. data/lib/farce/unsafe/tree_map.rb +2 -0
  120. data/lib/farce/unshareable.rb +8 -19
  121. data/lib/farce/unshared/atom.rb +3 -0
  122. data/lib/farce/unshared/lfu_map.rb +2 -0
  123. data/lib/farce/unshared/lru_map.rb +2 -0
  124. data/lib/farce/unshared/map.rb +3 -0
  125. data/lib/farce/unshared/pool.rb +44 -0
  126. data/lib/farce/unshared/scheduler.rb +89 -0
  127. data/lib/farce/unshared/set.rb +1 -0
  128. data/lib/farce/unshared/sorted_set.rb +1 -0
  129. data/lib/farce/unshared/tree_map.rb +5 -1
  130. data/lib/farce/unshared/vector.rb +3 -0
  131. data/lib/farce/unshared/weak_atom.rb +5 -1
  132. data/lib/farce/unshared/weak_key_map.rb +3 -0
  133. data/lib/farce/unshared/weak_map.rb +3 -0
  134. data/lib/farce/unshared/weak_set.rb +1 -0
  135. data/lib/farce/unshared/weak_value_map.rb +3 -0
  136. data/lib/farce/vector.rb +44 -19
  137. data/lib/farce/version.rb +1 -1
  138. data/lib/farce.rb +20 -25
  139. metadata +17 -2
@@ -3,7 +3,7 @@
3
3
  # warn_indent: true
4
4
 
5
5
  # @!group SortedSet Integration
6
- require "farce"
6
+ require "farce" unless defined?(Farce::Abstract)
7
7
  require "sorted_set"
8
8
 
9
9
  module Farce
@@ -3,7 +3,7 @@
3
3
  # warn_indent: true
4
4
 
5
5
  # @!group WeakRef Integration
6
- require "farce" unless defined?(Farce::Walker)
6
+ require "farce" unless defined?(Farce::Abstract)
7
7
  require "weakref" unless defined?(WeakRef)
8
8
 
9
9
  module Farce
@@ -21,6 +21,14 @@ module Farce
21
21
  end
22
22
  end
23
23
 
24
+ Farce::ModeManager::SmartRule.define(::WeakRef) do |value, mode_manager, mode|
25
+ ref = value.__getobj__
26
+ value = Farce::WeakRef.new(ref)
27
+ super(value, mode_manager, mode)
28
+ rescue ::WeakRef::RefError
29
+ Farce::WeakRef::RECYCLED
30
+ end
31
+
24
32
  Internal::Converter.define(::WeakRef, WeakRef) do
25
33
  def convert!(ref)
26
34
  @seen.fetch(ref) do
@@ -4,7 +4,7 @@
4
4
 
5
5
  # @!group Yajl Integration
6
6
  # Yajl 1.4.3 cannot encode or parse inside non-main native Ractors.
7
- require "farce"
8
- require "yajl" unless defined?(Yajl::Encoder)
9
- require "json" unless [].respond_to?(:to_json)
7
+ require "farce" unless defined?(Farce::Abstract)
8
+ require "yajl" unless defined?(Yajl::Encoder)
9
+ require "json" unless [].respond_to?(:to_json)
10
10
  require_relative "shared/to_json"
@@ -10,6 +10,7 @@ module Farce
10
10
  active_support: %w[active_support active_support/core_ext].freeze,
11
11
  bson: %w[bson].freeze,
12
12
  cbor: %w[cbor].freeze,
13
+ date: %w[date].freeze,
13
14
  dry_types: %w[dry/types].freeze,
14
15
  concurrent: %w[concurrent/map concurrent/tvar concurrent].freeze,
15
16
  json: %w[json].freeze,
@@ -61,28 +62,6 @@ module Farce
61
62
  end
62
63
  private_constant :RequireHook
63
64
 
64
- # A prepended singleton hook can recurse when Zeitwerk aliases it. Preserve
65
- # the original method explicitly instead of relying on super here.
66
- class << Kernel
67
- alias farce_original_require require
68
- private :farce_original_require
69
-
70
- # Load a feature and its integration through Kernel.require.
71
- # @api private
72
- # @param path [String, #to_path] The feature to load.
73
- # @return [Boolean] Whether the requested feature was newly loaded.
74
- def require(path, ...)
75
- path = File.path(path)
76
- result = Integrations.loading(path) { farce_original_require(path, ...) }
77
- integration = PATHS[path.delete_suffix(".rb")]
78
- if integration && !Integrations.loading?(integration) && Farce.config.autoload_integrations
79
- integration_path = "farce/integrations/#{integration}"
80
- Integrations.loading(integration_path) { farce_original_require(integration_path) }
81
- end
82
- result
83
- end
84
- end
85
-
86
65
  private_constant :INTEGRATIONS, :PATHS
87
66
 
88
67
  extend self
@@ -106,8 +85,39 @@ module Farce
106
85
  # Activate integrations at startup when automatic loading is enabled.
107
86
  # @api private
108
87
  def setup
88
+ return unless Farce.config.autoload_integrations
89
+ install_require_hooks
90
+ load_active
91
+ end
92
+
93
+ # Install hooks once, including when automatic loading is enabled later.
94
+ # @api private
95
+ def install_require_hooks
96
+ return if Object.ancestors.include?(RequireHook)
97
+ # The instance hook handles ordinary require calls. The singleton alias
98
+ # chain handles Kernel.require, which bypasses Object's instance methods.
109
99
  Object.prepend(RequireHook)
110
- load_active if Farce.config.autoload_integrations
100
+ # A prepended singleton hook can recurse when Zeitwerk aliases it. Preserve
101
+ # the original method explicitly instead of relying on super here.
102
+ class << Kernel
103
+ alias farce_original_require require
104
+ private :farce_original_require
105
+
106
+ # Load a feature and its integration through Kernel.require.
107
+ # @api private
108
+ # @param path [String, #to_path] The feature to load.
109
+ # @return [Boolean] Whether the requested feature was newly loaded.
110
+ def require(path, ...)
111
+ path = File.path(path)
112
+ result = Integrations.loading(path) { farce_original_require(path, ...) }
113
+ integration = PATHS[path.delete_suffix(".rb")]
114
+ if integration && !Integrations.loading?(integration) && Farce.config.autoload_integrations
115
+ integration_path = "farce/integrations/#{integration}"
116
+ Integrations.loading(integration_path) { farce_original_require(integration_path) }
117
+ end
118
+ result
119
+ end
120
+ end
111
121
  end
112
122
 
113
123
  # Loads all integrations for gems that have already been loaded.
@@ -0,0 +1,30 @@
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
+ module FlagWaiting
9
+ # Wait until the current value differs from the expected value.
10
+ # @param expected [Boolean] the value to wait to change from
11
+ # @param timeout [Numeric, nil] the total seconds available
12
+ # @yield called when the timeout expires
13
+ # @return [Boolean, BasicObject, nil] the changed value or timeout fallback
14
+ # @raise [ArgumentError] if expected is not true or false
15
+ def wait_until_changed(expected, timeout: nil)
16
+ unless expected.equal?(true) || expected.equal?(false)
17
+ raise ArgumentError, "expected value must be true or false"
18
+ end
19
+ signal = change_signal
20
+ Internal.with_timeout(timeout) do |_, deadline|
21
+ observed = signal.generation
22
+ current = value
23
+ return current unless expected == current
24
+ break unless signal.wait(observed, timeout: Internal.remaining_timeout(deadline))
25
+ end
26
+ yield if block_given?
27
+ end
28
+ end
29
+ end
30
+ end
@@ -4,7 +4,7 @@
4
4
 
5
5
  module Farce
6
6
  module Internal
7
- # Shared implementation for the explicit frozen-state policies in {Shareable}.
7
+ # Frozen-state policies for wrappers with separate backing storage.
8
8
  module Freeze
9
9
  # Structurally freeze a Ruby facade, then validate and publish its object graph.
10
10
  # This deliberately bypasses an override that controls logical frozen state.
@@ -20,14 +20,14 @@ module Farce
20
20
  raise FrozenError.new("can't modify frozen #{object.class}", receiver: object)
21
21
  end
22
22
 
23
- # Delegate logical frozen state to storage owned by the wrapper.
23
+ # Freeze owned storage and the facade without freezing stored values.
24
24
  module Delegated
25
25
  def freeze
26
26
  backend = freeze_backend
27
27
  raise "#{self.class} is not initialized" unless backend
28
28
 
29
29
  backend.freeze
30
- self
30
+ Object.instance_method(:freeze).bind_call(self)
31
31
  end
32
32
 
33
33
  def frozen?
@@ -39,6 +39,11 @@ module Farce
39
39
 
40
40
  def check_frozen! = Internal::Freeze.check(self)
41
41
 
42
+ def initialize_clone(other, freeze: nil)
43
+ super
44
+ self.freeze if is_a?(Farce::Unshareable) && (freeze.nil? ? other.frozen? : freeze)
45
+ end
46
+
42
47
  def publish_shareable_copy(other, operation:, freeze: nil)
43
48
  logically_frozen = operation == :clone && (freeze.nil? ? other.frozen? : freeze)
44
49
  self.freeze if logically_frozen
@@ -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
@@ -18,7 +18,7 @@ module Farce
18
18
  # @param compare_by_identity [Boolean] the default comparison mode for keys and values
19
19
  # @param compare_keys_by_identity [Boolean] whether keys are compared by identity
20
20
  # @param compare_values_by_identity [Boolean] whether values are compared by identity
21
- def initialize(initial_mapping = nil, mode: :copy, normalize_keys: nil, compare_by_identity: false,
21
+ def initialize(initial_mapping = nil, mode: :smart, normalize_keys: nil, compare_by_identity: false,
22
22
  compare_keys_by_identity: compare_by_identity, compare_values_by_identity: compare_by_identity)
23
23
  initial_mapping = convert_entries(initial_mapping)
24
24
 
@@ -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:)
@@ -15,7 +15,7 @@ module Farce
15
15
  module SchedulerLifecycle
16
16
  include Farce::Unshareable
17
17
 
18
- # Prepending bypasses Unshareable's include hook, so remove freeze here.
18
+ # A live scheduler must keep updating its own lifecycle state.
19
19
  def self.prepended(scheduler)
20
20
  scheduler.undef_method(:freeze)
21
21
  end
@@ -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|
data/lib/farce/lazy.rb CHANGED
@@ -5,7 +5,7 @@
5
5
  module Farce
6
6
  # A ractor-shareable lazy value that is computed on demand.
7
7
  # The factory runs once. Its result is transferred according to {#mode}.
8
- # With the default `:copy` mode, each Ractor receives its own cached copy.
8
+ # The default `:smart` mode prepares results using smart rules or dedicated proxies.
9
9
  # The factory and its bound receiver must remain Ractor-shareable.
10
10
  # Use {Strict::Lazy} for direct shareable results or {Unshared::Lazy} for local factories.
11
11
  #
@@ -63,15 +63,15 @@ module Farce
63
63
  include Shareable::Tracked
64
64
 
65
65
  # @!macro modes
66
- # @overload initialize(factory, mode: :copy)
66
+ # @overload initialize(factory, mode: :smart)
67
67
  # @param factory [Class, Proc, #call] the shareable factory for the value
68
68
  # @param mode [Symbol] the transfer mode for the computed result
69
- # @overload initialize(mode: :copy, self: nil)
69
+ # @overload initialize(mode: :smart, self: nil)
70
70
  # @param mode [Symbol] the transfer mode for the computed result
71
71
  # @param self [BasicObject] the shareable receiver to bind to the block
72
72
  # @yield computes the value on first access
73
73
  # @yieldreturn [BasicObject] the result to transfer
74
- def initialize(factory = nil, mode: :copy, **, &)
74
+ def initialize(factory = nil, mode: :smart, **, &)
75
75
  @manager = marshal_mode_manager(mode)
76
76
  super(factory, **, &)
77
77
  end
@@ -6,7 +6,7 @@ module Farce
6
6
  # @!parse
7
7
  # # A {Reference} for a {Lazy} value.
8
8
  # #
9
- # # Results use the lazy value's transfer mode, which defaults to `:copy`.
9
+ # # Results use the lazy value's transfer mode, which defaults to `:smart`.
10
10
  # #
11
11
  # # Calling `freeze` resolves and freezes the target. Checking `frozen?` before
12
12
  # # the first successful freeze does not resolve it.
@@ -24,11 +24,11 @@ module Farce
24
24
  # # initialized.value # => 1
25
25
  # class LazyRef < Farce::Reference
26
26
  # # @!macro modes
27
- # # @overload initialize(factory, mode: :copy)
27
+ # # @overload initialize(factory, mode: :smart)
28
28
  # # @param mode [Symbol] the transfer mode for the computed result
29
29
  # # @param [Class, Proc, #call] factory The factory to use for creating the value. Must be ractor-shareable.
30
30
  # #
31
- # # @overload initialize(self: nil, mode: :copy)
31
+ # # @overload initialize(self: nil, mode: :smart)
32
32
  # # @param mode [Symbol] the transfer mode for the computed result
33
33
  # # @yield The block to use for creating the value. Must be ractor-shareable.
34
34
  # # @yieldreceiver [BasicObject] The `self` parameter provided, or `nil` if not provided.
data/lib/farce/lfu_map.rb CHANGED
@@ -13,7 +13,7 @@ module Farce
13
13
  # @param entries [Hash, Array<Array(BasicObject, BasicObject)>, Abstract::Map, #each, nil]
14
14
  # Optional initial entries. Entries are stored sequentially and may be evicted.
15
15
  # @param mode [Symbol] The default value transfer mode.
16
- def initialize(entries = nil, mode: :copy, **)
16
+ def initialize(entries = nil, mode: :smart, **)
17
17
  @manager = marshal_mode_manager(mode)
18
18
  super(entries, **)
19
19
  end
@@ -35,6 +35,9 @@ module Farce
35
35
  # @return [Boolean] the new value
36
36
  def store(value) = scoped_value.store(value)
37
37
 
38
+ # (see Abstract::Flag#wait_until_changed)
39
+ def wait_until_changed(...) = scoped_value.wait_until_changed(...)
40
+
38
41
  alias value= store
39
42
 
40
43
  # Replace the value and return its previous state.
@@ -60,6 +63,7 @@ module Farce
60
63
  end
61
64
 
62
65
  def new_scoped_value(...) = Internal::Flag.new(...)
66
+ def change_signal = scoped_value.change_signal
63
67
  end
64
68
  end
65
69
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Local
7
+ # A shareable handle with an independent thread pool in each scope.
8
+ #
9
+ # Each pool uses the scaling settings and fiber scheduler configured at
10
+ # construction. Tasks always keep their block and argument references. Only
11
+ # local and smart modes are accepted when `auto_local` is false. A scheduler
12
+ # constructor must be convertible to a shareable proc for use in each Ractor.
13
+ # {#close} drains only the current scope's pool. {#close_all} drains every pool
14
+ # across all scopes and Ractors, and prevents new pools from starting.
15
+ #
16
+ # @example Keep task state within each Ractor
17
+ # pool = Farce::Local::Pool.new(max_size: 2)
18
+ # results = Farce::Unshared::Queue.new
19
+ # pool.schedule { results.push(:done) }
20
+ # results.pop # => :done
21
+ # pool.close_all
22
+ class Pool < Abstract::Pool
23
+ include Shareable::Unfreezable
24
+ include Scoped
25
+ include Internal::LocalExecutorMethods
26
+
27
+ # (see Farce::Abstract::Pool#initialize)
28
+ # @!macro scopes
29
+ # @param scope [Symbol] The scope of the pool. Defaults to `:ractor`.
30
+ def initialize(min_size: 0, max_size: 4, max_inflight: 64,
31
+ grow_after: 0.005, shrink_after: 30, capacity: 1024, scope: :ractor,
32
+ backend: Internal::FROZEN_CONFIG.io_backend, &constructor)
33
+ initialize_configuration(min_size:, max_size:, max_inflight:, grow_after:, shrink_after:, backend:)
34
+ @constructor = prepare_constructor(constructor || Internal::FROZEN_CONFIG.fiber_scheduler_constructor)
35
+ @pools = Strict::Map.new
36
+ @closed_all = Internal::Flag.new(false)
37
+ @initialization_key = Object.new.freeze
38
+ super(scope:, min_size: @min_size, max_size: @max_size, max_inflight: @max_inflight,
39
+ grow_after: @grow_after, shrink_after: @shrink_after, capacity:, backend:)
40
+ end
41
+
42
+ # Stop accepting work in this scope and drain its queued tasks.
43
+ # @return [self]
44
+ def close
45
+ pool = @closed_all.value ? Internal::Storage[self, scope:] : scoped_value
46
+ pool&.close
47
+ self
48
+ end
49
+
50
+ # Stop accepting work in every scope and Ractor and request draining.
51
+ # Future scopes also reject submissions. Does not wait for running tasks.
52
+ # @return [self]
53
+ def close_all
54
+ return self unless @closed_all.compare_and_set(false, true)
55
+ @pools.each_pair do |pool, queue|
56
+ queue.push([pool, :close].freeze, clock: 0)
57
+ rescue ClosedQueueError
58
+ # The owning Ractor may already have shut down its supervisor.
59
+ nil
60
+ end
61
+ close
62
+ end
63
+
64
+ # @return [Integer] Running and starting workers in this scope.
65
+ def size = current_pool&.size || 0
66
+
67
+ # @return [Symbol] The current scope's pool state.
68
+ def state = current_pool&.state || :closed
69
+
70
+ # @return [Exception, nil] The first worker error in this scope.
71
+ def error = current_pool&.error
72
+
73
+ # @return [Boolean] Whether this scope rejects new tasks.
74
+ def closed? = @closed_all.value || super
75
+
76
+ Internal.delegate(self, :scoped_value, :drained?, :admission_generation, :wait_for_admission,
77
+ :admission_changed, :task_taken, :retire_worker, :worker_stopped, :scale_if_needed)
78
+
79
+ private
80
+
81
+ def enqueue_task(non_block, ...)
82
+ raise PoolClosedError, "cannot schedule task on a closed pool" if @closed_all.value
83
+ pool = scoped_value
84
+ submitted = non_block ? pool.try_schedule(...) : pool.schedule(...)
85
+ submitted && self
86
+ end
87
+
88
+ def current_pool
89
+ @closed_all.value ? Internal::Storage[self, scope:] : scoped_value
90
+ end
91
+
92
+ # Worker setup can yield. Initialize once when fibers share a scope.
93
+ def scoped_value
94
+ storage = Internal::Storage.scope(scope)
95
+ pool = storage[self]
96
+ return pool if pool
97
+
98
+ initializers = Internal::Storage.store_if_absent(@initialization_key) { Internal::Storage::ThreadSafe.new }
99
+ initializers.store_if_absent(storage) { Internal::LeaseInitialization.new }.fetch { super }
100
+ end
101
+
102
+ def new_scoped_value(**)
103
+ pool = Unshared::Pool.new(**, &@constructor)
104
+ queue, reference = Internal::ThreadPoolSupervisor.current.close_handle(pool)
105
+ @pools[reference] = queue
106
+ pool.close if @closed_all.value
107
+ pool
108
+ end
109
+ end
110
+ end
111
+ end