221 lines
5.9 KiB
Ruby
221 lines
5.9 KiB
Ruby
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
|