This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
require 'concurrent/concern/logging'
|
||||
|
||||
module Concurrent
|
||||
module Concern
|
||||
|
||||
# @!visibility private
|
||||
# @!macro internal_implementation_note
|
||||
module Deprecation
|
||||
# TODO require additional parameter: a version. Display when it'll be removed based on that. Error if not removed.
|
||||
include Concern::Logging
|
||||
|
||||
def deprecated(message, strip = 2)
|
||||
caller_line = caller(strip).first if strip > 0
|
||||
klass = if Module === self
|
||||
self
|
||||
else
|
||||
self.class
|
||||
end
|
||||
message = if strip > 0
|
||||
format("[DEPRECATED] %s\ncalled on: %s", message, caller_line)
|
||||
else
|
||||
format('[DEPRECATED] %s', message)
|
||||
end
|
||||
log WARN, klass.to_s, message
|
||||
end
|
||||
|
||||
def deprecated_method(old_name, new_name)
|
||||
deprecated "`#{old_name}` is deprecated and it'll removed in next release, use `#{new_name}` instead", 3
|
||||
end
|
||||
|
||||
extend self
|
||||
end
|
||||
end
|
||||
end
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
module Concurrent
|
||||
module Concern
|
||||
|
||||
# Object references in Ruby are mutable. This can lead to serious problems when
|
||||
# the `#value` of a concurrent object is a mutable reference. Which is always the
|
||||
# case unless the value is a `Fixnum`, `Symbol`, or similar "primitive" data type.
|
||||
# Most classes in this library that expose a `#value` getter method do so using the
|
||||
# `Dereferenceable` mixin module.
|
||||
#
|
||||
# @!macro copy_options
|
||||
module Dereferenceable
|
||||
# NOTE: This module is going away in 2.0. In the mean time we need it to
|
||||
# play nicely with the synchronization layer. This means that the
|
||||
# including class SHOULD be synchronized and it MUST implement a
|
||||
# `#synchronize` method. Not doing so will lead to runtime errors.
|
||||
|
||||
# Return the value this object represents after applying the options specified
|
||||
# by the `#set_deref_options` method.
|
||||
#
|
||||
# @return [Object] the current value of the object
|
||||
def value
|
||||
synchronize { apply_deref_options(@value) }
|
||||
end
|
||||
alias_method :deref, :value
|
||||
|
||||
protected
|
||||
|
||||
# Set the internal value of this object
|
||||
#
|
||||
# @param [Object] value the new value
|
||||
def value=(value)
|
||||
synchronize{ @value = value }
|
||||
end
|
||||
|
||||
# @!macro dereferenceable_set_deref_options
|
||||
# Set the options which define the operations #value performs before
|
||||
# returning data to the caller (dereferencing).
|
||||
#
|
||||
# @note Most classes that include this module will call `#set_deref_options`
|
||||
# from within the constructor, thus allowing these options to be set at
|
||||
# object creation.
|
||||
#
|
||||
# @param [Hash] opts the options defining dereference behavior.
|
||||
# @option opts [String] :dup_on_deref (false) call `#dup` before returning the data
|
||||
# @option opts [String] :freeze_on_deref (false) call `#freeze` before returning the data
|
||||
# @option opts [String] :copy_on_deref (nil) call the given `Proc` passing
|
||||
# the internal value and returning the value returned from the proc
|
||||
def set_deref_options(opts = {})
|
||||
synchronize{ ns_set_deref_options(opts) }
|
||||
end
|
||||
|
||||
# @!macro dereferenceable_set_deref_options
|
||||
# @!visibility private
|
||||
def ns_set_deref_options(opts)
|
||||
@dup_on_deref = opts[:dup_on_deref] || opts[:dup]
|
||||
@freeze_on_deref = opts[:freeze_on_deref] || opts[:freeze]
|
||||
@copy_on_deref = opts[:copy_on_deref] || opts[:copy]
|
||||
@do_nothing_on_deref = !(@dup_on_deref || @freeze_on_deref || @copy_on_deref)
|
||||
nil
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def apply_deref_options(value)
|
||||
return nil if value.nil?
|
||||
return value if @do_nothing_on_deref
|
||||
value = @copy_on_deref.call(value) if @copy_on_deref
|
||||
value = value.dup if @dup_on_deref
|
||||
value = value.freeze if @freeze_on_deref
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,121 @@
|
||||
require 'concurrent/atomic/atomic_reference'
|
||||
|
||||
module Concurrent
|
||||
module Concern
|
||||
|
||||
# Include where logging is needed
|
||||
#
|
||||
# @!visibility private
|
||||
module Logging
|
||||
# The same as Logger::Severity but we copy it here to avoid a dependency on the logger gem just for these 7 constants
|
||||
DEBUG, INFO, WARN, ERROR, FATAL, UNKNOWN = 0, 1, 2, 3, 4, 5
|
||||
SEV_LABEL = %w[DEBUG INFO WARN ERROR FATAL ANY].freeze
|
||||
|
||||
# Logs through {Concurrent.global_logger}, it can be overridden by setting @logger
|
||||
# @param [Integer] level one of Concurrent::Concern::Logging constants
|
||||
# @param [String] progname e.g. a path of an Actor
|
||||
# @param [String, nil] message when nil block is used to generate the message
|
||||
# @yieldreturn [String] a message
|
||||
def log(level, progname, message = nil, &block)
|
||||
logger = if defined?(@logger) && @logger
|
||||
@logger
|
||||
else
|
||||
Concurrent.global_logger
|
||||
end
|
||||
logger.call level, progname, message, &block
|
||||
rescue => error
|
||||
$stderr.puts "`Concurrent.global_logger` failed to log #{[level, progname, message, block]}\n" +
|
||||
"#{error.message} (#{error.class})\n#{error.backtrace.join "\n"}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Concurrent
|
||||
extend Concern::Logging
|
||||
|
||||
# Create a simple logger with provided level and output.
|
||||
def self.create_simple_logger(level = :FATAL, output = $stderr)
|
||||
level = Concern::Logging.const_get(level) unless level.is_a?(Integer)
|
||||
|
||||
# TODO (pitr-ch 24-Dec-2016): figure out why it had to be replaced, stdlogger was deadlocking
|
||||
lambda do |severity, progname, message = nil, &block|
|
||||
return false if severity < level
|
||||
|
||||
message = block ? block.call : message
|
||||
formatted_message = case message
|
||||
when String
|
||||
message
|
||||
when Exception
|
||||
format "%s (%s)\n%s",
|
||||
message.message, message.class, (message.backtrace || []).join("\n")
|
||||
else
|
||||
message.inspect
|
||||
end
|
||||
|
||||
output.print format "[%s] %5s -- %s: %s\n",
|
||||
Time.now.strftime('%Y-%m-%d %H:%M:%S.%L'),
|
||||
Concern::Logging::SEV_LABEL[severity],
|
||||
progname,
|
||||
formatted_message
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
# Use logger created by #create_simple_logger to log concurrent-ruby messages.
|
||||
def self.use_simple_logger(level = :FATAL, output = $stderr)
|
||||
Concurrent.global_logger = create_simple_logger level, output
|
||||
end
|
||||
|
||||
# Create a stdlib logger with provided level and output.
|
||||
# If you use this deprecated method you might need to add logger to your Gemfile to avoid warnings from Ruby 3.3.5+.
|
||||
# @deprecated
|
||||
def self.create_stdlib_logger(level = :FATAL, output = $stderr)
|
||||
require 'logger'
|
||||
logger = Logger.new(output)
|
||||
logger.level = level
|
||||
logger.formatter = lambda do |severity, datetime, progname, msg|
|
||||
formatted_message = case msg
|
||||
when String
|
||||
msg
|
||||
when Exception
|
||||
format "%s (%s)\n%s",
|
||||
msg.message, msg.class, (msg.backtrace || []).join("\n")
|
||||
else
|
||||
msg.inspect
|
||||
end
|
||||
format "[%s] %5s -- %s: %s\n",
|
||||
datetime.strftime('%Y-%m-%d %H:%M:%S.%L'),
|
||||
severity,
|
||||
progname,
|
||||
formatted_message
|
||||
end
|
||||
|
||||
lambda do |loglevel, progname, message = nil, &block|
|
||||
logger.add loglevel, message, progname, &block
|
||||
end
|
||||
end
|
||||
|
||||
# Use logger created by #create_stdlib_logger to log concurrent-ruby messages.
|
||||
# @deprecated
|
||||
def self.use_stdlib_logger(level = :FATAL, output = $stderr)
|
||||
Concurrent.global_logger = create_stdlib_logger level, output
|
||||
end
|
||||
|
||||
# TODO (pitr-ch 27-Dec-2016): remove deadlocking stdlib_logger methods
|
||||
|
||||
# Suppresses all output when used for logging.
|
||||
NULL_LOGGER = lambda { |level, progname, message = nil, &block| }
|
||||
|
||||
# @!visibility private
|
||||
GLOBAL_LOGGER = AtomicReference.new(create_simple_logger(:WARN))
|
||||
private_constant :GLOBAL_LOGGER
|
||||
|
||||
def self.global_logger
|
||||
GLOBAL_LOGGER.value
|
||||
end
|
||||
|
||||
def self.global_logger=(value)
|
||||
GLOBAL_LOGGER.value = value
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,220 @@
|
||||
require 'thread'
|
||||
require 'timeout'
|
||||
|
||||
require 'concurrent/atomic/event'
|
||||
require 'concurrent/concern/dereferenceable'
|
||||
|
||||
module Concurrent
|
||||
module Concern
|
||||
|
||||
module Obligation
|
||||
include Concern::Dereferenceable
|
||||
# NOTE: The Dereferenceable module is going away in 2.0. In the mean time
|
||||
# we need it to place nicely with the synchronization layer. This means
|
||||
# that the including class SHOULD be synchronized and it MUST implement a
|
||||
# `#synchronize` method. Not doing so will lead to runtime errors.
|
||||
|
||||
# Has the obligation been fulfilled?
|
||||
#
|
||||
# @return [Boolean]
|
||||
def fulfilled?
|
||||
state == :fulfilled
|
||||
end
|
||||
alias_method :realized?, :fulfilled?
|
||||
|
||||
# Has the obligation been rejected?
|
||||
#
|
||||
# @return [Boolean]
|
||||
def rejected?
|
||||
state == :rejected
|
||||
end
|
||||
|
||||
# Is obligation completion still pending?
|
||||
#
|
||||
# @return [Boolean]
|
||||
def pending?
|
||||
state == :pending
|
||||
end
|
||||
|
||||
# Is the obligation still unscheduled?
|
||||
#
|
||||
# @return [Boolean]
|
||||
def unscheduled?
|
||||
state == :unscheduled
|
||||
end
|
||||
|
||||
# Has the obligation completed processing?
|
||||
#
|
||||
# @return [Boolean]
|
||||
def complete?
|
||||
[:fulfilled, :rejected].include? state
|
||||
end
|
||||
|
||||
# Is the obligation still awaiting completion of processing?
|
||||
#
|
||||
# @return [Boolean]
|
||||
def incomplete?
|
||||
! complete?
|
||||
end
|
||||
|
||||
# The current value of the obligation. Will be `nil` while the state is
|
||||
# pending or the operation has been rejected.
|
||||
#
|
||||
# @param [Numeric] timeout the maximum time in seconds to wait.
|
||||
# @return [Object] see Dereferenceable#deref
|
||||
def value(timeout = nil)
|
||||
wait timeout
|
||||
deref
|
||||
end
|
||||
|
||||
# Wait until obligation is complete or the timeout has been reached.
|
||||
#
|
||||
# @param [Numeric] timeout the maximum time in seconds to wait.
|
||||
# @return [Obligation] self
|
||||
def wait(timeout = nil)
|
||||
event.wait(timeout) if timeout != 0 && incomplete?
|
||||
self
|
||||
end
|
||||
|
||||
# Wait until obligation is complete or the timeout is reached. Will re-raise
|
||||
# any exceptions raised during processing (but will not raise an exception
|
||||
# on timeout).
|
||||
#
|
||||
# @param [Numeric] timeout the maximum time in seconds to wait.
|
||||
# @return [Obligation] self
|
||||
# @raise [Exception] raises the reason when rejected
|
||||
def wait!(timeout = nil)
|
||||
wait(timeout).tap { raise self if rejected? }
|
||||
end
|
||||
alias_method :no_error!, :wait!
|
||||
|
||||
# The current value of the obligation. Will be `nil` while the state is
|
||||
# pending or the operation has been rejected. Will re-raise any exceptions
|
||||
# raised during processing (but will not raise an exception on timeout).
|
||||
#
|
||||
# @param [Numeric] timeout the maximum time in seconds to wait.
|
||||
# @return [Object] see Dereferenceable#deref
|
||||
# @raise [Exception] raises the reason when rejected
|
||||
def value!(timeout = nil)
|
||||
wait(timeout)
|
||||
if rejected?
|
||||
raise self
|
||||
else
|
||||
deref
|
||||
end
|
||||
end
|
||||
|
||||
# The current state of the obligation.
|
||||
#
|
||||
# @return [Symbol] the current state
|
||||
def state
|
||||
synchronize { @state }
|
||||
end
|
||||
|
||||
# If an exception was raised during processing this will return the
|
||||
# exception object. Will return `nil` when the state is pending or if
|
||||
# the obligation has been successfully fulfilled.
|
||||
#
|
||||
# @return [Exception] the exception raised during processing or `nil`
|
||||
def reason
|
||||
synchronize { @reason }
|
||||
end
|
||||
|
||||
# @example allows Obligation to be risen
|
||||
# rejected_ivar = Ivar.new.fail
|
||||
# raise rejected_ivar
|
||||
def exception(*args)
|
||||
raise 'obligation is not rejected' unless rejected?
|
||||
reason.exception(*args)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @!visibility private
|
||||
def get_arguments_from(opts = {})
|
||||
[*opts.fetch(:args, [])]
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def init_obligation
|
||||
@event = Event.new
|
||||
@value = @reason = nil
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def event
|
||||
@event
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def set_state(success, value, reason)
|
||||
if success
|
||||
@value = value
|
||||
@state = :fulfilled
|
||||
else
|
||||
@reason = reason
|
||||
@state = :rejected
|
||||
end
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def state=(value)
|
||||
synchronize { ns_set_state(value) }
|
||||
end
|
||||
|
||||
# Atomic compare and set operation
|
||||
# State is set to `next_state` only if `current state == expected_current`.
|
||||
#
|
||||
# @param [Symbol] next_state
|
||||
# @param [Symbol] expected_current
|
||||
#
|
||||
# @return [Boolean] true is state is changed, false otherwise
|
||||
#
|
||||
# @!visibility private
|
||||
def compare_and_set_state(next_state, *expected_current)
|
||||
synchronize do
|
||||
if expected_current.include? @state
|
||||
@state = next_state
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Executes the block within mutex if current state is included in expected_states
|
||||
#
|
||||
# @return block value if executed, false otherwise
|
||||
#
|
||||
# @!visibility private
|
||||
def if_state(*expected_states)
|
||||
synchronize do
|
||||
raise ArgumentError.new('no block given') unless block_given?
|
||||
|
||||
if expected_states.include? @state
|
||||
yield
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Am I in the current state?
|
||||
#
|
||||
# @param [Symbol] expected The state to check against
|
||||
# @return [Boolean] true if in the expected state else false
|
||||
#
|
||||
# @!visibility private
|
||||
def ns_check_state?(expected)
|
||||
@state == expected
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def ns_set_state(value)
|
||||
@state = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,110 @@
|
||||
require 'concurrent/collection/copy_on_notify_observer_set'
|
||||
require 'concurrent/collection/copy_on_write_observer_set'
|
||||
|
||||
module Concurrent
|
||||
module Concern
|
||||
|
||||
# The [observer pattern](http://en.wikipedia.org/wiki/Observer_pattern) is one
|
||||
# of the most useful design patterns.
|
||||
#
|
||||
# The workflow is very simple:
|
||||
# - an `observer` can register itself to a `subject` via a callback
|
||||
# - many `observers` can be registered to the same `subject`
|
||||
# - the `subject` notifies all registered observers when its status changes
|
||||
# - an `observer` can deregister itself when is no more interested to receive
|
||||
# event notifications
|
||||
#
|
||||
# In a single threaded environment the whole pattern is very easy: the
|
||||
# `subject` can use a simple data structure to manage all its subscribed
|
||||
# `observer`s and every `observer` can react directly to every event without
|
||||
# caring about synchronization.
|
||||
#
|
||||
# In a multi threaded environment things are more complex. The `subject` must
|
||||
# synchronize the access to its data structure and to do so currently we're
|
||||
# using two specialized ObserverSet: {Concurrent::Concern::CopyOnWriteObserverSet}
|
||||
# and {Concurrent::Concern::CopyOnNotifyObserverSet}.
|
||||
#
|
||||
# When implementing and `observer` there's a very important rule to remember:
|
||||
# **there are no guarantees about the thread that will execute the callback**
|
||||
#
|
||||
# Let's take this example
|
||||
# ```
|
||||
# class Observer
|
||||
# def initialize
|
||||
# @count = 0
|
||||
# end
|
||||
#
|
||||
# def update
|
||||
# @count += 1
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# obs = Observer.new
|
||||
# [obj1, obj2, obj3, obj4].each { |o| o.add_observer(obs) }
|
||||
# # execute [obj1, obj2, obj3, obj4]
|
||||
# ```
|
||||
#
|
||||
# `obs` is wrong because the variable `@count` can be accessed by different
|
||||
# threads at the same time, so it should be synchronized (using either a Mutex
|
||||
# or an AtomicFixum)
|
||||
module Observable
|
||||
|
||||
# @!macro observable_add_observer
|
||||
#
|
||||
# Adds an observer to this set. If a block is passed, the observer will be
|
||||
# created by this method and no other params should be passed.
|
||||
#
|
||||
# @param [Object] observer the observer to add
|
||||
# @param [Symbol] func the function to call on the observer during notification.
|
||||
# Default is :update
|
||||
# @return [Object] the added observer
|
||||
def add_observer(observer = nil, func = :update, &block)
|
||||
observers.add_observer(observer, func, &block)
|
||||
end
|
||||
|
||||
# As `#add_observer` but can be used for chaining.
|
||||
#
|
||||
# @param [Object] observer the observer to add
|
||||
# @param [Symbol] func the function to call on the observer during notification.
|
||||
# @return [Observable] self
|
||||
def with_observer(observer = nil, func = :update, &block)
|
||||
add_observer(observer, func, &block)
|
||||
self
|
||||
end
|
||||
|
||||
# @!macro observable_delete_observer
|
||||
#
|
||||
# Remove `observer` as an observer on this object so that it will no
|
||||
# longer receive notifications.
|
||||
#
|
||||
# @param [Object] observer the observer to remove
|
||||
# @return [Object] the deleted observer
|
||||
def delete_observer(observer)
|
||||
observers.delete_observer(observer)
|
||||
end
|
||||
|
||||
# @!macro observable_delete_observers
|
||||
#
|
||||
# Remove all observers associated with this object.
|
||||
#
|
||||
# @return [Observable] self
|
||||
def delete_observers
|
||||
observers.delete_observers
|
||||
self
|
||||
end
|
||||
|
||||
# @!macro observable_count_observers
|
||||
#
|
||||
# Return the number of observers associated with this object.
|
||||
#
|
||||
# @return [Integer] the observers count
|
||||
def count_observers
|
||||
observers.count_observers
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_accessor :observers
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user