farce 0.1.0-x86-linux-gnu → 0.3.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 +127 -1
- data/README.md +111 -17
- data/docs/gems/dry-types.md +8 -8
- data/docs/modes.md +75 -15
- data/docs/scopes.md +3 -1
- data/docs/variants.md +18 -0
- data/ext/farce/atom.c +37 -22
- data/ext/farce/bounded_map.c +17 -13
- data/ext/farce/containers.h +15 -0
- data/ext/farce/counter.c +13 -18
- data/ext/farce/exchanger.c +5 -1
- data/ext/farce/farce.c +1 -1
- data/ext/farce/flag.c +39 -3
- data/ext/farce/io.c +3 -3
- data/ext/farce/lock.c +17 -23
- data/ext/farce/map.c +88 -66
- data/ext/farce/priority_queue.c +55 -43
- data/ext/farce/queue.c +10 -4
- data/ext/farce/reactor.c +52 -25
- data/ext/farce/reactor.h +1 -1
- data/ext/farce/signal.c +5 -1
- data/ext/farce/transaction.c +20 -24
- data/ext/farce/tree_map.c +33 -18
- data/ext/farce/trie.c +81 -43
- data/ext/farce/unshareable.c +168 -19
- data/ext/farce/unshared_io_pool.h +21 -12
- data/ext/farce/unshared_signal.c +13 -2
- data/ext/farce/unshared_wait.h +3 -0
- data/ext/farce/vector.c +82 -18
- data/ext/farce/weak_map.c +75 -63
- data/lib/farce/_yard/macros.rb +18 -3
- data/lib/farce/abstract/executor.rb +130 -0
- data/lib/farce/abstract/flag.rb +73 -0
- data/lib/farce/abstract/pool.rb +334 -0
- data/lib/farce/abstract/queue.rb +4 -3
- data/lib/farce/abstract/scheduler.rb +539 -93
- data/lib/farce/abstract/set.rb +17 -5
- data/lib/farce/abstract/sorted_set.rb +2 -2
- data/lib/farce/abstract/timer_queue.rb +49 -10
- data/lib/farce/abstract/vector.rb +510 -28
- data/lib/farce/atom.rb +1 -1
- data/lib/farce/class_mirror.rb +20 -7
- data/lib/farce/config.rb +7 -1
- data/lib/farce/engine/jruby/signal.rb +7 -4
- data/lib/farce/engine/jvm/flag.rb +15 -3
- data/lib/farce/engine/ruby/3.4/farce.so +0 -0
- data/lib/farce/engine/ruby/4.0/farce.so +0 -0
- data/lib/farce/engine/ruby/shared/require_hook.rb +18 -0
- data/lib/farce/engine/ruby.rb +7 -0
- data/lib/farce/engine/shared/main_scheduler.rb +2 -2
- data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
- data/lib/farce/engine/shared/proxy_owner.rb +1 -1
- data/lib/farce/engine/shared/unshared_atom.rb +9 -8
- data/lib/farce/engine/shared/unshared_vector.rb +31 -4
- data/lib/farce/engine/shared/vector.rb +8 -0
- data/lib/farce/engine/shared/weak_map/index.rb +14 -1
- data/lib/farce/engine/shared.rb +9 -0
- data/lib/farce/engine/truffleruby/native/flag.rb +15 -3
- data/lib/farce/envelope.rb +54 -7
- data/lib/farce/exchanger.rb +1 -1
- data/lib/farce/flag.rb +2 -0
- data/lib/farce/integrations/active_support.rb +1 -1
- data/lib/farce/integrations/bson.rb +1 -1
- data/lib/farce/integrations/cbor.rb +1 -1
- data/lib/farce/integrations/concurrent.rb +3 -3
- data/lib/farce/integrations/date.rb +8 -0
- data/lib/farce/integrations/dry_types.rb +7 -4
- data/lib/farce/integrations/json.rb +2 -2
- data/lib/farce/integrations/msgpack.rb +1 -1
- data/lib/farce/integrations/oj.rb +3 -4
- data/lib/farce/integrations/psych.rb +1 -1
- data/lib/farce/integrations/ractor_sharing.rb +1 -1
- data/lib/farce/integrations/sorted_set.rb +1 -1
- data/lib/farce/integrations/weakref.rb +9 -1
- data/lib/farce/integrations/yajl.rb +3 -3
- data/lib/farce/integrations.rb +33 -23
- data/lib/farce/internal/flag_waiting.rb +30 -0
- data/lib/farce/internal/freeze.rb +8 -3
- 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/map_value_modes.rb +1 -1
- data/lib/farce/internal/scheduled_task.rb +1 -1
- data/lib/farce/internal/scheduler_lifecycle.rb +1 -1
- 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/lazy.rb +4 -4
- data/lib/farce/lazy_ref.rb +3 -3
- data/lib/farce/lfu_map.rb +1 -1
- data/lib/farce/local/flag.rb +4 -0
- data/lib/farce/local/pool.rb +111 -0
- data/lib/farce/local/scheduler.rb +219 -0
- data/lib/farce/local/scoped.rb +1 -1
- data/lib/farce/lru_map.rb +1 -1
- data/lib/farce/map.rb +2 -2
- data/lib/farce/mode_manager/smart_rule.rb +138 -0
- data/lib/farce/mode_manager.rb +12 -3
- data/lib/farce/molecule.rb +2 -2
- data/lib/farce/pool.rb +2 -290
- data/lib/farce/port.rb +4 -4
- data/lib/farce/priority_queue.rb +1 -1
- data/lib/farce/proxy/register.rb +6 -0
- data/lib/farce/proxy/supervisor.rb +1 -1
- data/lib/farce/proxy.rb +9 -4
- data/lib/farce/queue.rb +1 -1
- data/lib/farce/scheduler.rb +1 -502
- data/lib/farce/set.rb +1 -1
- data/lib/farce/sorted_set.rb +1 -1
- data/lib/farce/task.rb +285 -0
- data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
- data/lib/farce/timer_queue.rb +20 -6
- data/lib/farce/transaction/vector.rb +43 -9
- data/lib/farce/transaction/wrapper.rb +14 -0
- data/lib/farce/tree_map.rb +1 -1
- data/lib/farce/unsafe/lfu_map.rb +2 -0
- data/lib/farce/unsafe/lru_map.rb +2 -0
- data/lib/farce/unsafe/tree_map.rb +2 -0
- data/lib/farce/unshareable.rb +8 -19
- data/lib/farce/unshared/atom.rb +3 -0
- data/lib/farce/unshared/lfu_map.rb +2 -0
- data/lib/farce/unshared/lru_map.rb +2 -0
- data/lib/farce/unshared/map.rb +3 -0
- data/lib/farce/unshared/pool.rb +44 -0
- data/lib/farce/unshared/scheduler.rb +89 -0
- data/lib/farce/unshared/set.rb +1 -0
- data/lib/farce/unshared/sorted_set.rb +1 -0
- data/lib/farce/unshared/tree_map.rb +5 -1
- data/lib/farce/unshared/vector.rb +3 -0
- data/lib/farce/unshared/weak_atom.rb +5 -1
- data/lib/farce/unshared/weak_key_map.rb +3 -0
- data/lib/farce/unshared/weak_map.rb +3 -0
- data/lib/farce/unshared/weak_set.rb +1 -0
- data/lib/farce/unshared/weak_value_map.rb +3 -0
- data/lib/farce/vector.rb +44 -19
- data/lib/farce/version.rb +1 -1
- data/lib/farce.rb +20 -25
- metadata +17 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# warn_indent: true
|
|
4
4
|
|
|
5
5
|
# @!group WeakRef Integration
|
|
6
|
-
require "farce" unless defined?(Farce::
|
|
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"
|
|
9
|
-
require "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"
|
data/lib/farce/integrations.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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: :
|
|
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
|
-
#
|
|
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
|
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|
|
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
|
-
#
|
|
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: :
|
|
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: :
|
|
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: :
|
|
74
|
+
def initialize(factory = nil, mode: :smart, **, &)
|
|
75
75
|
@manager = marshal_mode_manager(mode)
|
|
76
76
|
super(factory, **, &)
|
|
77
77
|
end
|
data/lib/farce/lazy_ref.rb
CHANGED
|
@@ -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 `:
|
|
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: :
|
|
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: :
|
|
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: :
|
|
16
|
+
def initialize(entries = nil, mode: :smart, **)
|
|
17
17
|
@manager = marshal_mode_manager(mode)
|
|
18
18
|
super(entries, **)
|
|
19
19
|
end
|
data/lib/farce/local/flag.rb
CHANGED
|
@@ -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
|