diff options
Diffstat (limited to 'lib/singleton.rb')
| -rw-r--r-- | lib/singleton.rb | 387 |
1 files changed, 210 insertions, 177 deletions
diff --git a/lib/singleton.rb b/lib/singleton.rb index 404eaf7101..74aec8903c 100644 --- a/lib/singleton.rb +++ b/lib/singleton.rb @@ -1,207 +1,240 @@ -# The Singleton module implements the Singleton pattern - i.e. +# frozen_string_literal: true + +# The Singleton module implements the Singleton pattern. +# +# == Usage +# +# To use Singleton, include the module in your class. +# +# class Klass +# include Singleton +# # ... +# end # -# class Klass -# include Singleton -# # ... -# end +# This ensures that only one instance of Klass can be created. # -# * ensures that only one instance of Klass called ``the instance'' -# can be created. +# a,b = Klass.instance, Klass.instance # -# a,b = Klass.instance, Klass.instance -# a == b # => true -# a.new # NoMethodError - new is private ... +# a == b +# # => true # -# * ``The instance'' is created at instanciation time - i.e. the first call -# of Klass.instance(). +# Klass.new +# # => NoMethodError - new is private ... # -# class OtherKlass +# The instance is created at upon the first call of Klass.instance(). +# +# class OtherKlass # include Singleton # # ... -# end -# p "#{ObjectSpace.each_object(OtherKlass) {}}" # => 0 +# end +# +# ObjectSpace.each_object(OtherKlass){} +# # => 0 +# +# OtherKlass.instance +# ObjectSpace.each_object(OtherKlass){} +# # => 1 +# # -# * This behavior is preserved under inheritance. +# This behavior is preserved under inheritance and cloning. # +# == Implementation # -# This achieved by marking -# * Klass.new and Klass.allocate - as private and modifying -# * Klass.inherited(sub_klass) - to ensure -# that the Singleton pattern is properly inherited. +# This above is achieved by: # -# In addition Klass is provided with the class methods -# * Klass.instance() - returning ``the instance'' -# * Klass._load(str) - returning ``the instance'' -# * Klass._wait() - a hook method putting a second (or n-th) -# thread calling Klass.instance on a waiting loop if the first call -# to Klass.instance is still in progress. +# * Making Klass.new and Klass.allocate private. # -# The sole instance method of Singleton is -# * _dump(depth) - returning the empty string -# The default Marshalling strategy is to strip all state information - i.e. -# instance variables from ``the instance''. Providing custom -# _dump(depth) and _load(str) method allows the (partial) resurrection -# of a previous state of ``the instance'' - see third example. +# * Overriding Klass.inherited(sub_klass) and Klass.clone() to ensure that the +# Singleton properties are kept when inherited and cloned. +# +# * Providing the Klass.instance() method that returns the same object each +# time it is called. +# +# * Overriding Klass._load(str) to call Klass.instance(). +# +# * Overriding Klass#clone and Klass#dup to raise TypeErrors to prevent +# cloning or duping. +# +# == Singleton and Marshal +# +# By default Singleton's #_dump(depth) returns the empty string. Marshalling by +# default will strip state information, e.g. instance variables from the instance. +# Classes using Singleton can provide custom _load(str) and _dump(depth) methods +# to retain some of the previous state of the instance. +# +# require 'singleton' +# +# class Example +# include Singleton +# attr_accessor :keep, :strip +# def _dump(depth) +# # this strips the @strip information from the instance +# Marshal.dump(@keep, depth) +# end +# +# def self._load(str) +# instance.keep = Marshal.load(str) +# instance +# end +# end +# +# a = Example.instance +# a.keep = "keep this" +# a.strip = "get rid of this" +# +# stored_state = Marshal.dump(a) +# +# a.keep = nil +# a.strip = nil +# b = Marshal.load(stored_state) +# p a == b # => true +# p a.keep # => "keep this" +# p a.strip # => nil # module Singleton - def Singleton.included (klass) - # should this be checked? - # raise TypeError.new "..." if klass.type == Module - klass.module_eval { - undef_method :clone - undef_method :dup - } - class << klass - def inherited(sub_klass) - # @__instance__ takes on one of the following values - # * nil - before (and after a failed) creation - # * false - during creation - # * sub_class instance - after a successful creation - sub_klass.instance_eval { @__instance__ = nil } - def sub_klass.instance - unless @__instance__.nil? - # is the extra flexiblity having the hook method - # _wait() around ever useful? - _wait() - # check for instance creation - return @__instance__ if @__instance__ - end - Thread.critical = true - unless @__instance__ - @__instance__ = false - Thread.critical = false - begin - @__instance__ = new - ensure - if @__instance__ - define_method(:instance) {@__instance__ } - else - # failed instance creation - @__instance__ = nil - end - end - else - Thread.critical = false - end - return @__instance__ - end - end - def _load(str) - instance - end - def _wait - sleep(0.05) while false.equal?(@__instance__) - end - private :new, :allocate - # hook methods are also marked private - private :_load,:_wait + # The version string + VERSION = "0.3.0" + + module SingletonInstanceMethods # :nodoc: + # Raises a TypeError to prevent cloning. + def clone + raise TypeError, "can't clone instance of singleton #{self.class}" + end + + # Raises a TypeError to prevent duping. + def dup + raise TypeError, "can't dup instance of singleton #{self.class}" + end + + # By default, do not retain any state when marshalling. + def _dump(depth = -1) + '' end - klass.inherited klass end - private - def _dump(depth) - return "" + include SingletonInstanceMethods + + module SingletonClassMethods # :nodoc: + + def clone # :nodoc: + Singleton.__init__(super) + end + + # By default calls instance(). Override to retain singleton state. + def _load(str) + instance + end + + def instance # :nodoc: + @singleton__instance__ || @singleton__mutex__.synchronize { @singleton__instance__ ||= new } + end + + private + + def inherited(sub_klass) + super + Singleton.__init__(sub_klass) + end + + def set_instance(val) + @singleton__instance__ = val + end + + def set_mutex(val) + @singleton__mutex__ = val + end + end + + def self.module_with_class_methods # :nodoc: + SingletonClassMethods end -end -if __FILE__ == $0 + module SingletonClassProperties # :nodoc: -#basic example -class SomeSingletonClass - include Singleton -end -a = SomeSingletonClass.instance -b = SomeSingletonClass.instance # a and b are same object -p a == b # => true -begin - SomeSingletonClass.new -rescue NoMethodError => mes - puts mes -end + def self.included(c) + # extending an object with Singleton is a bad idea + c.undef_method :extend_object + end -# threaded example with exception and customized hook #_wait method -Thread.abort_on_exception = false -def num_of_instances(mod) - "#{ObjectSpace.each_object(mod){}} #{mod} instance" -end - -class Ups < SomeSingletonClass - def initialize - type.__sleep - puts "initialize called by thread ##{Thread.current[:i]}" - end - class << self - def _wait - @enter.push Thread.current[:i] - sleep 0.02 while false.equal?(@__instance__) - @leave.push Thread.current[:i] - end - def __sleep - sleep (rand(0.1)) - end - def allocate - __sleep - def self.allocate; __sleep; super() end - raise "allocation in thread ##{Thread.current[:i]} aborted" - end - def instanciate_all - @enter = [] - @leave = [] - 1.upto(9) do |i| - Thread.new do - begin - Thread.current[:i] = i - __sleep - instance - rescue RuntimeError => mes - puts mes - end - end - end - puts "Before there were #{num_of_instances(Ups)}s" - sleep 3 - puts "Now there is #{num_of_instances(Ups)}" - puts "#{@enter.join "; "} was the order of threads entering the waiting loop" - puts "#{@leave.join "; "} was the order of threads leaving the waiting loop" - end + def self.extended(c) + # extending an object with Singleton is a bad idea + c.singleton_class.send(:undef_method, :extend_object) end -end -Ups.instanciate_all -# results in message like -# Before there were 0 Ups instances -# boom - allocation in thread #8 aborted -# initialize called by thread #3 -# Now there is 1 Ups instance -# 2; 3; 6; 1; 7; 5; 9; 4 was the order of threads entering the waiting loop -# 3; 2; 1; 7; 6; 5; 4; 9 was the order of threads leaving the waiting loop + def __init__(klass) # :nodoc: + klass.instance_eval { + set_instance(nil) + set_mutex(Thread::Mutex.new) + } + klass + end + private -# Customized marshalling -class A - include Singleton - attr_accessor :persist, :die - def _dump(depth) - # this strips the @die information from the instance - Marshal.dump(@persist,depth) + def append_features(mod) + # help out people counting on transitive mixins + unless mod.instance_of?(Class) + raise TypeError, "Inclusion of the OO-Singleton module in module #{mod}" + end + super end + + def included(klass) + super + klass.private_class_method :new, :allocate + klass.extend module_with_class_methods + Singleton.__init__(klass) + end + end + extend SingletonClassProperties + + ## + # :singleton-method: _load + # By default calls instance(). Override to retain singleton state. + + ## + # :singleton-method: instance + # Returns the singleton instance. end -def A._load(str) - instance.persist = Marshal.load(str) - instance -end -a = A.instance -a.persist = ["persist"] -a.die = "die" +if defined?(Ractor) + module RactorLocalSingleton # :nodoc: + include Singleton::SingletonInstanceMethods + + module RactorLocalSingletonClassMethods # :nodoc: + include Singleton::SingletonClassMethods + def instance + set_mutex(Thread::Mutex.new) if Ractor.current[mutex_key].nil? + return Ractor.current[instance_key] if Ractor.current[instance_key] + Ractor.current[mutex_key].synchronize { + return Ractor.current[instance_key] if Ractor.current[instance_key] + set_instance(new()) + } + Ractor.current[instance_key] + end + + private + + def instance_key + :"__RactorLocalSingleton_instance_with_class_id_#{object_id}__" + end + + def mutex_key + :"__RactorLocalSingleton_mutex_with_class_id_#{object_id}__" + end -stored_state = Marshal.dump(a) -# change state -a.persist = nil -a.die = nil -b = Marshal.load(stored_state) -p a == b # => true -p a.persist # => ["persist"] -p a.die # => nil + def set_instance(val) + Ractor.current[instance_key] = val + end + def set_mutex(val) + Ractor.current[mutex_key] = val + end + end + + def self.module_with_class_methods + RactorLocalSingletonClassMethods + end + + extend Singleton::SingletonClassProperties + end end |
