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!