module Concurrent # Provides tools for cooperative cancellation. # Inspired by https://msdn.microsoft.com/en-us/library/dd537607(v=vs.110).aspx # @example # # Create new cancellation. `cancellation` is used for cancelling, `token` is passed down to # # tasks for cooperative cancellation # cancellation, token = Concurrent::Cancellation.create # Thread.new(token) do |token| # # Count 1+1 (simulating some other meaningful work) repeatedly until the token is cancelled through # # cancellation. # token.loop_until_canceled { 1+1 } # end # sleep 0.1 # cancellation.cancel # Stop the thread by cancelling class Cancellation < Synchronization::Object safe_initialization! # Creates the cancellation object. Returns both the cancellation and the token for convenience. # @param [Object] resolve_args resolve_args Arguments which are used when resolve method is called on # resolvable_future_or_event # @param [Promises::Resolvable] resolvable_future_or_event resolvable used to track cancellation. # Can be retrieved by `token.to_future` ot `token.to_event`. # @example # cancellation, token = Concurrent::Cancellation.create # @return [Array(Cancellation, Cancellation::Token)] def self.create(resolvable_future_or_event = Promises.resolvable_event, *resolve_args) cancellation = new(resolvable_future_or_event, *resolve_args) [cancellation, cancellation.token] end private_class_method :new # Returns the token associated with the cancellation. # @return [Token] def token @Token end # Cancel this cancellation. All executions depending on the token will cooperatively stop. # @return [true, false] # @raise when cancelling for the second tim def cancel(raise_on_repeated_call = true) !!@Cancel.resolve(*@ResolveArgs, raise_on_repeated_call) end # Is the cancellation cancelled? # @return [true, false] def canceled? @Cancel.resolved? end # Short string representation. # @return [String] def to_s format '<#%s:0x%x canceled:%s>', self.class, object_id << 1, canceled? end alias_method :inspect, :to_s private def initialize(future, *resolve_args) raise ArgumentError, 'future is not Resolvable' unless future.is_a?(Promises::Resolvable) @Cancel = future @Token = Token.new @Cancel.with_hidden_resolvable @ResolveArgs = resolve_args end # Created through {Cancellation.create}, passed down to tasks to be able to check if canceled. class Token < Synchronization::Object safe_initialization! # @return [Event] Event which will be resolved when the token is cancelled. def to_event @Cancel.to_event end # @return [Future] Future which will be resolved when the token is cancelled with arguments passed in # {Cancellation.create} . def to_future @Cancel.to_future end # Is the token cancelled? # @return [true, false] def canceled? @Cancel.resolved? end # Repeatedly evaluates block until the token is {#canceled?}. # @yield to the block repeatedly. # @yieldreturn [Object] # @return [Object] last result of the block def loop_until_canceled(&block) until canceled? result = block.call end result end # Raise error when cancelled # @param [#exception] error to be risen # @raise the error # @return [self] def raise_if_canceled(error = CancelledOperationError) raise error if canceled? self end # Creates a new token which is cancelled when any of the tokens is. # @param [Token] tokens to combine # @return [Token] new token def join(*tokens, &block) block ||= -> tokens { Promises.any_event(*tokens.map(&:to_event)) } self.class.new block.call([@Cancel, *tokens]) end # Short string representation. # @return [String] def to_s format '<#%s:0x%x canceled:%s>', self.class, object_id << 1, canceled? end alias_method :inspect, :to_s private def initialize(cancel) @Cancel = cancel end end # FIXME (pitr-ch 27-Mar-2016): cooperation with mutex, condition, select etc? # TODO (pitr-ch 27-Mar-2016): examples (scheduled to be cancelled in 10 sec) end end