From ff881c0ce5f160d69e54433431332b668562214f Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 17 Dec 2025 12:10:31 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Make=20Config.global=20Ractor=20?= =?UTF-8?q?shareable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Config.global now delegates to a frozen config, and updates are handled by swapping that frozen config for a new one. --- lib/net/imap/config.rb | 3 +- lib/net/imap/config/attr_accessors.rb | 1 - lib/net/imap/config/attr_type_coercion.rb | 2 +- lib/net/imap/config/attr_version_defaults.rb | 11 ++-- lib/net/imap/config/global.rb | 57 ++++++++++++++++++++ test/net/imap/test_config.rb | 12 ++++- 6 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 lib/net/imap/config/global.rb diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index 29c01bd40..d936f47ee 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -4,6 +4,7 @@ require_relative "config/attr_inheritance" require_relative "config/attr_type_coercion" require_relative "config/attr_version_defaults" +require_relative "config/global" module Net class IMAP @@ -632,7 +633,7 @@ def defaults_hash end @default = AttrVersionDefaults.compile_default! - @global = default.new + @global = Global.setup! AttrVersionDefaults.compile_version_defaults! end diff --git a/lib/net/imap/config/attr_accessors.rb b/lib/net/imap/config/attr_accessors.rb index 8725428ce..a47466a0c 100644 --- a/lib/net/imap/config/attr_accessors.rb +++ b/lib/net/imap/config/attr_accessors.rb @@ -33,7 +33,6 @@ def self.attr_accessor(name) # :nodoc: internal API def self.attributes instance_methods.grep(/=\z/).map { _1.to_s.delete_suffix("=").to_sym } end - private_class_method :attributes def self.struct # :nodoc: internal API unless defined?(self::Struct) diff --git a/lib/net/imap/config/attr_type_coercion.rb b/lib/net/imap/config/attr_type_coercion.rb index 282ec7f0d..15fc2a456 100644 --- a/lib/net/imap/config/attr_type_coercion.rb +++ b/lib/net/imap/config/attr_type_coercion.rb @@ -45,7 +45,7 @@ def self.safe(&b) else def self.safe(&b) nil.instance_eval(&b).freeze end end - private_class_method :safe + # private_class_method :safe Types = Hash.new do |h, type| type => Proc | nil; safe{type} end Types[:boolean] = Boolean = safe{-> {!!_1}} diff --git a/lib/net/imap/config/attr_version_defaults.rb b/lib/net/imap/config/attr_version_defaults.rb index 626b94f16..d0e6346a3 100644 --- a/lib/net/imap/config/attr_version_defaults.rb +++ b/lib/net/imap/config/attr_version_defaults.rb @@ -26,14 +26,15 @@ module AttrVersionDefaults # See Config.version_defaults. singleton_class.attr_reader :version_defaults - @version_defaults = Hash.new {|h, k| + @version_defaults = {} + version_defaults.default_proc = AttrTypeCoercion.safe {->(h, k){ # NOTE: String responds to both so the order is significant. # And ignore non-numeric conversion to zero, because: "wat!?".to_r == 0 (h.fetch(k.to_r, nil) || h.fetch(k.to_f, nil) if k.is_a?(Numeric)) || - (h.fetch(k.to_sym, nil) if k.respond_to?(:to_sym)) || - (h.fetch(k.to_r, nil) if k.respond_to?(:to_r) && k.to_r != 0r) || - (h.fetch(k.to_f, nil) if k.respond_to?(:to_f) && k.to_f != 0.0) - } + (h.fetch(k.to_sym, nil) if k.respond_to?(:to_sym)) || + (h.fetch(k.to_r, nil) if k.respond_to?(:to_r) && k.to_r != 0r) || + (h.fetch(k.to_f, nil) if k.respond_to?(:to_f) && k.to_f != 0.0) + }} # :stopdoc: internal APIs only diff --git a/lib/net/imap/config/global.rb b/lib/net/imap/config/global.rb new file mode 100644 index 000000000..956b3000c --- /dev/null +++ b/lib/net/imap/config/global.rb @@ -0,0 +1,57 @@ +require "singleton" + +module Net + class IMAP + class Config + class Global < Config + include Singleton + extend Forwardable + singleton_class.extend(Forwardable) + + singleton_class.attr_reader :snapshot + + def_delegators :"self.class", :reset, :snapshot + protected :snapshot + + def self.setup! + @snapshot = Config.default.new.freeze + AttrAccessors.attributes.each do |attr| + singleton_class.define_method(:"#{attr}=") do |val| + @snapshot = snapshot.dup.update(attr => val).freeze + end + singleton_class.def_delegator :snapshot, attr + def_delegators :"self.class", attr, :"#{attr}=" + end + instance + end + + def initialize + super(Config.default) + @data = nil + freeze + end + + def new(**attrs) Config.new(self, **attrs) end + + def self.reset(attr = nil) + if attr.nil? + @snapshot = Config.default.new.freeze + self + elsif snapshot.inherited?(attr) + nil + else + old, new = send(attr), snapshot.dup + new.reset(attr) + @snapshot = new.freeze + old + end + end + + protected + + def data = snapshot.data + + end + end + end +end diff --git a/test/net/imap/test_config.rb b/test/net/imap/test_config.rb index 6a35bff9e..9bcfe4f95 100644 --- a/test/net/imap/test_config.rb +++ b/test/net/imap/test_config.rb @@ -88,7 +88,7 @@ class ConfigTest < Net::IMAP::TestCase assert_equal true, global.debug? global.reset(:debug) assert_equal false, global.debug? - refute global.frozen? + assert global.frozen? end test "Net::IMAP.config" do @@ -157,6 +157,16 @@ class ConfigTest < Net::IMAP::TestCase end end + if defined?(Ractor) + test ".global is deeply frozen (and Ractor shareable)" do + assert Ractor.shareable?(Config.global) + end + + test ".version_defaults is deeply frozen (and Ractor shareable)" do + assert Ractor.shareable? Config.version_defaults + end + end + test "Config[:default] and Config[:current] both hold default config" do defaults = Config.default.to_h assert_equal(defaults, Config[:default].to_h)