lib/adhearsion/call_controller/dial.rb in adhearsion-2.4.0.beta1 vs lib/adhearsion/call_controller/dial.rb in adhearsion-2.4.0.beta2

- old
+ new

@@ -74,37 +74,40 @@ def initialize(to, options, call) raise Call::Hangup unless call.alive? && call.active? @options, @call = options, call @targets = to.respond_to?(:has_key?) ? to : Array(to) + @call_targets = {} set_defaults end - def set_defaults - @status = DialStatus.new - - @latch = CountDownLatch.new @targets.size - - @options[:from] ||= @call.from - - _for = @options.delete :for - @options[:timeout] ||= _for if _for - - @confirmation_controller = @options.delete :confirm - @confirmation_metadata = @options.delete :confirm_metadata + def inspect + "#<#{self.class} to=#{@to.inspect} options=#{@options.inspect}>" end + # Prep outbound calls, link call lifecycles and place outbound calls def run track_originating_call prep_calls place_calls end + # + # Links the lifecycle of the originating call to the Dial operation such that the Dial is unblocked when the originating call ends def track_originating_call - @call.on_end { |_| @latch.countdown! until @latch.count == 0 } + @call.on_end do |_| + logger.info "Root call ended, unblocking everything..." + @waiters.each do |latch| + latch.countdown! until latch.count == 0 + end + end end + # + # Prepares a set of OutboundCall actors to be dialed and links their lifecycles to the Dial operation + # + # @yield Each call to the passed block for further setup operations def prep_calls @calls = @targets.map do |target, specific_options| new_call = OutboundCall.new join_status = JoinStatus.new @@ -120,13 +123,15 @@ new_call.on_answer do |event| pre_confirmation_tasks new_call new_call.on_unjoined @call do |unjoined| - new_call["dial_countdown_#{@call.id}"] = true join_status.ended - @latch.countdown! + unless @splitting + new_call["dial_countdown_#{@call.id}"] = true + @latch.countdown! + end end if @confirmation_controller status.unconfirmed! join_status.unconfirmed! @@ -139,35 +144,111 @@ logger.info "#dial joining call #{new_call.id} to #{@call.id}" pre_join_tasks new_call @call.answer join_status.started new_call.join @call - status.answer!(new_call) + status.answer! elsif status.result == :answer join_status.lost_confirmation! end end - [new_call, target, specific_options] + @call_targets[new_call] = [target, specific_options] + + yield new_call if block_given? + + new_call end status.calls = @calls end + # + # Dials the set of outbound calls def place_calls - @calls.map! do |call, target, specific_options| + @calls.each do |call| + target, specific_options = @call_targets[call] local_options = @options.dup.deep_merge specific_options if specific_options call.dial target, (local_options || @options) - call end end + # Split calls party to the dial + # Marks the end time in the status of each join, but does not unblock #dial until one of the calls ends + # Optionally executes call controllers on calls once split, where 'current_dial' is available in controller metadata in order to perform further operations on the Dial, including rejoining and termination. + # @param [Hash] targets Target call controllers to execute on call legs once split + # @option options [Adhearsion::CallController] :main The call controller class to execute on the 'main' call leg (the one who initiated the #dial) + # @option options [Proc] :main_callback A block to call when the :main controller completes + # @option options [Adhearsion::CallController] :others The call controller class to execute on the 'other' call legs (the ones created as a result of the #dial) + # @option options [Proc] :others_callback A block to call when the :others controller completes on an individual call + def split(targets = {}) + logger.info "Splitting calls apart" + @splitting = true + @calls.each do |call| + logger.info "Unjoining peer #{call.id}" + ignoring_missing_joins { @call.unjoin call.id } + if split_controller = targets[:others] + logger.info "Executing split controller #{split_controller} on #{call.id}" + call.execute_controller split_controller.new(call, 'current_dial' => self), targets[:others_callback] + end + end + if split_controller = targets[:main] + logger.info "Executing split controller #{split_controller} on main call" + @call.execute_controller split_controller.new(@call, 'current_dial' => self), targets[:main_callback] + end + end + + # Rejoin parties that were previously split + # @param [Call, String, Hash] target The target to join calls to. See Call#join for details. + def rejoin(target = @call) + logger.info "Rejoining to #{target}" + unless target == @call + @call.join target + end + @calls.each do |call| + call.join target + end + end + + # Merge another Dial into this one, joining all calls to a mixer + # @param [Dial] other the other dial operation to merge calls from + def merge(other) + logger.info "Merging with #{other.inspect}" + mixer_name = SecureRandom.uuid + + split + other.split + + rejoin mixer_name: mixer_name + other.rejoin mixer_name: mixer_name + + calls_to_merge = other.status.calls + [other.root_call] + @calls.concat calls_to_merge + + latch = CountDownLatch.new calls_to_merge.size + calls_to_merge.each do |call| + call.on_end { |event| latch.countdown! } + end + @waiters << latch + end + + # + # Block until the dial operation is completed by an appropriate quorum of the involved calls ending def await_completion @latch.wait(@options[:timeout]) || status.timeout! - @latch.wait if status.result == :answer + return unless status.result == :answer + @waiters.each(&:wait) end + # + # Do not hangup outbound calls when the Dial operation finishes. This allows outbound calls to continue with other processing once they are unjoined. + def skip_cleanup + @skip_cleanup = true + end + + # + # Hangup any remaining calls def cleanup_calls calls_to_hangup = @calls.map do |call| begin [call.id, call] if call.active? rescue Celluloid::DeadActorError @@ -175,22 +256,49 @@ end.compact if calls_to_hangup.size.zero? logger.info "#dial finished with no remaining outbound calls" return end - logger.info "#dial finished. Hanging up #{calls_to_hangup.size} outbound calls which are still active: #{calls_to_hangup.map(&:first).join ", "}." - calls_to_hangup.each do |id, outbound_call| - begin - outbound_call.hangup - rescue Celluloid::DeadActorError - # This actor may previously have been shut down due to the call ending + if @skip_cleanup + logger.info "#dial finished. Leaving #{calls_to_hangup.size} outbound calls going which are still active: #{calls_to_hangup.map(&:first).join ", "}." + else + logger.info "#dial finished. Hanging up #{calls_to_hangup.size} outbound calls which are still active: #{calls_to_hangup.map(&:first).join ", "}." + calls_to_hangup.each do |id, outbound_call| + begin + outbound_call.hangup + rescue Celluloid::DeadActorError + # This actor may previously have been shut down due to the call ending + end end end end + protected + + def root_call + @call + end + private + def set_defaults + @status = DialStatus.new + + @latch = CountDownLatch.new @targets.size + @waiters = [@latch] + + @options[:from] ||= @call.from + + _for = @options.delete :for + @options[:timeout] ||= _for if _for + + @confirmation_controller = @options.delete :confirm + @confirmation_metadata = @options.delete :confirm_metadata + + @skip_cleanup = false + end + def pre_confirmation_tasks(call) on_all_except call do |target_call| logger.info "#dial hanging up call #{target_call.id} because this call has been answered by another channel" target_call.hangup end @@ -198,29 +306,35 @@ def pre_join_tasks(call) end def on_all_except(call) - @calls.each do |target_call, _| + @calls.each do |target_call| begin next if target_call.id == call.id yield target_call rescue Celluloid::DeadActorError # This actor may previously have been shut down due to the call ending end end end + + def ignoring_missing_joins + yield + rescue Punchblock::ProtocolError => e + raise unless e.name == :service_unavailable + end end class ParallelConfirmationDial < Dial + private + def set_defaults super @apology_controller = @options.delete :apology end - private - def pre_confirmation_tasks(call) end def pre_join_tasks(call) on_all_except call do |target_call| @@ -235,11 +349,11 @@ end end class DialStatus # The collection of calls created during the dial operation - attr_accessor :calls, :joined_call + attr_accessor :calls # A collection of status objects indexed by call. Provides status on the joins such as duration attr_accessor :joins # @private @@ -255,11 +369,10 @@ def result @result || :no_answer end # @private - def answer!(call) - @joined_call = call + def answer! @result = :answer end # @private def timeout!