class ElectricSlide::CallQueue

Constants

AGENT_RETURN_METHODS
CONNECTION_TYPES
DuplicateAgentError
ENDED_CALL_EXCEPTIONS
Error
MissingAgentError

Attributes

agent_return_method[R]
agent_strategy[R]
connection_type[R]

Public Class Methods

new(opts = {}) click to toggle source
# File lib/electric_slide/call_queue.rb, line 75
def initialize(opts = {})
  @agents = []      # Needed to keep track of global list of agents
  @queue = []       # Calls waiting for an agent

  update(
    agent_strategy: opts[:agent_strategy] || AgentStrategy::LongestIdle,
    connection_type: opts[:connection_type] || :call,
    agent_return_method: opts[:agent_return_method] || :auto
  )
end
valid_agent_return_method?(agent_return_method) click to toggle source
# File lib/electric_slide/call_queue.rb, line 71
def self.valid_agent_return_method?(agent_return_method)
  AGENT_RETURN_METHODS.include? agent_return_method
end
valid_connection_type?(connection_type) click to toggle source
# File lib/electric_slide/call_queue.rb, line 67
def self.valid_connection_type?(connection_type)
  CONNECTION_TYPES.include? connection_type
end
valid_with?(attrs = {}) click to toggle source
# File lib/electric_slide/call_queue.rb, line 47
def self.valid_with?(attrs = {})
  return false unless Hash === attrs

  if agent_strategy = attrs[:agent_strategy]
    begin
      agent_strategy.new
    rescue Exception
      return false
    end
  end
  if connection_type = attrs[:connection_type]
    return false unless valid_connection_type? connection_type
  end
  if agent_return_method = attrs[:agent_return_method]
    return false unless valid_agent_return_method? agent_return_method
  end

  true
end

Public Instance Methods

add_agent(agent) click to toggle source

Registers an agent to the queue @param [Agent] agent The agent to be added to the queue @raise ArgumentError if the agent is malformed @raise DuplicateAgentError if this agent ID already exists @see update_agent

# File lib/electric_slide/call_queue.rb, line 163
def add_agent(agent)
  abort ArgumentError.new("#add_agent called with nil object") if agent.nil?
  abort DuplicateAgentError.new("Agent is already in the queue") if get_agent(agent.id)

  agent.queue = current_actor

  case @connection_type
  when :call
    abort ArgumentError.new("Agent has no callable address") unless agent.address
  when :bridge
    bridged_agent_health_check agent
  end

  logger.info "Adding agent #{agent} to the queue"
  @agents << agent
  @strategy << agent if agent.presence == :available
  # Fake the presence callback since this is a new agent
  agent.callback :presence_change, current_actor, agent.call, agent.presence, :unavailable

  async.check_for_connections
end
agent_available?() click to toggle source

Checks whether an agent is available to take a call @return [Boolean] True if an agent is available

# File lib/electric_slide/call_queue.rb, line 116
def agent_available?
  @strategy.agent_available?
end
agent_return_method=(new_agent_return_method) click to toggle source
# File lib/electric_slide/call_queue.rb, line 109
def agent_return_method=(new_agent_return_method)
  abort InvalidRequeueMethod.new unless CallQueue.valid_agent_return_method? new_agent_return_method
  @agent_return_method = new_agent_return_method
end
agent_strategy=(new_agent_strategy) click to toggle source
# File lib/electric_slide/call_queue.rb, line 93
def agent_strategy=(new_agent_strategy)
  @agent_strategy = new_agent_strategy

  @strategy = @agent_strategy.new
  @agents.each do |agent|
    return_agent agent, agent.presence
  end

  @agent_strategy
end
available_agent_summary() click to toggle source

Returns information about the number of available agents The data returned depends on the AgentStrategy in use. The data will always include a :total count of the agents available @return [Hash] Summary information about agents available, depending on strategy

# File lib/electric_slide/call_queue.rb, line 124
def available_agent_summary
  # TODO: Make this a delegator?
  @strategy.available_agent_summary
end
call_waiting?() click to toggle source

Checks whether any callers are waiting @return [Boolean] True if a caller is waiting

# File lib/electric_slide/call_queue.rb, line 334
def call_waiting?
  @queue.length > 0
end
calls_waiting() click to toggle source

Returns the number of callers waiting in the queue @return [Fixnum]

# File lib/electric_slide/call_queue.rb, line 340
def calls_waiting
  @queue.length
end
check_for_connections() click to toggle source

Checks to see if any callers are waiting for an agent and attempts to connect them to an available agent

# File lib/electric_slide/call_queue.rb, line 231
def check_for_connections
  connect checkout_agent, get_next_caller while call_waiting? && agent_available?
end
checkout_agent() click to toggle source

Assigns the first available agent, marking the agent :on_call @return {Agent}

# File lib/electric_slide/call_queue.rb, line 131
def checkout_agent
  agent = @strategy.checkout_agent
  if agent
    agent.update_presence(:on_call)
  end
  agent
end
conditionally_return_agent(agent, return_method = @agent_return_method) click to toggle source
# File lib/electric_slide/call_queue.rb, line 314
def conditionally_return_agent(agent, return_method = @agent_return_method)
  raise ArgumentError, "Invalid requeue method; must be one of #{AGENT_RETURN_METHODS.join ','}" unless AGENT_RETURN_METHODS.include? return_method

  if agent && @agents.include?(agent) && agent.on_call? && return_method == :auto
    logger.info "Returning agent #{agent.id} to queue"
    return_agent agent
  else
    logger.debug "Not returning agent #{agent.inspect} to the queue"
    return_agent agent, :after_call
  end
end
connect(agent, queued_call) click to toggle source

Connect an {Agent} to a caller @param [Agent] agent Agent to be connected @param [Adhearsion::Call] call Caller to be connected

# File lib/electric_slide/call_queue.rb, line 278
def connect(agent, queued_call)
  unless queued_call.active?
    logger.warn "Inactive queued call found in #connect"
    return_agent agent
  end

  queued_call[:agent] = agent

  logger.info "Connecting #{agent} with #{remote_party queued_call}"
  case @connection_type
  when :call
    call_agent agent, queued_call
  when :bridge
    unless agent.call && agent.call.active?
      logger.warn "Inactive agent call found in #connect, returning caller to queue"
      priority_enqueue queued_call
    end
    bridge_agent agent, queued_call
  end
rescue *ENDED_CALL_EXCEPTIONS
  ignoring_ended_calls do
    if queued_call.active?
      logger.warn "Dead call exception in #connect but queued_call still alive, reinserting into queue"
      priority_enqueue queued_call
    end
  end
  ignoring_ended_calls do
    if agent.call && agent.call.active?
      logger.warn "Dead call exception in #connect but agent call still alive, reinserting into queue"
      agent.callback :connection_failed, current_actor, agent.call, queued_call

      return_agent agent
    end
  end
end
connection_type=(new_connection_type) click to toggle source
# File lib/electric_slide/call_queue.rb, line 104
def connection_type=(new_connection_type)
  abort InvalidConnectionType.new unless CallQueue.valid_connection_type? new_connection_type
  @connection_type = new_connection_type
end
enqueue(call) click to toggle source

Add a call to the end of the queue, the normal FIFO queue behavior @param [Adhearsion::Call] call Caller to be added to the queue

# File lib/electric_slide/call_queue.rb, line 253
def enqueue(call)
  ignoring_ended_calls do
    logger.info "Adding call from #{remote_party call} to the queue"
    call[:electric_slide_enqueued_at] = DateTime.now
    call.on_end { remove_call call }
    @queue << call unless @queue.include? call

    check_for_connections
  end
end
get_agent(id) click to toggle source

Finds an agent known to the queue by that agent’s ID @param [String] id The ID of the agent to locate @return [Agent, Nil] {Agent} object if found, Nil otherwise

# File lib/electric_slide/call_queue.rb, line 154
def get_agent(id)
  @agents.detect { |agent| agent.id == id }
end
get_agents() click to toggle source

Returns a copy of the set of agents that are known to the queue @return [Array] Array of {Agent} objects

# File lib/electric_slide/call_queue.rb, line 141
def get_agents
  @agents.dup
end
get_next_caller() click to toggle source

Returns the next waiting caller @return [Adhearsion::Call] The next waiting caller

# File lib/electric_slide/call_queue.rb, line 328
def get_next_caller
  @queue.shift
end
get_queued_calls() click to toggle source

Returns a copy of the set of calls waiting to be answered that are known to the queue @return [Array] Array of Adhearsion::Call objects

# File lib/electric_slide/call_queue.rb, line 147
def get_queued_calls
  @queue.dup
end
priority_enqueue(call) click to toggle source

Add a call to the head of the queue. Among other reasons, this is used when a caller is sent to an agent, but the connection fails because the agent is not available. @param [Adhearsion::Call] call Caller to be added to the queue

# File lib/electric_slide/call_queue.rb, line 238
def priority_enqueue(call)
  # In case this is a re-insert on agent failure...
  # ... reset `:agent` call variable
  call[:agent] = nil
  # ... set, but don't reset, the enqueue time
  call[:electric_slide_enqueued_at] ||= DateTime.now

  call.on_end { remove_call call }
  @queue.unshift call

  check_for_connections
end
remove_agent(agent, extra_params = {}) click to toggle source

Removes an agent from the queue entirely @param [Agent] agent The {Agent} to be removed from the queue @param [Hash] extra_params Application specific extra params @return [Agent, Nil] The Agent object if removed, Nil otherwise

# File lib/electric_slide/call_queue.rb, line 221
def remove_agent(agent, extra_params = {})
  agent.update_presence(:unavailable, extra_params)
  @strategy.delete agent
  @agents.delete agent
  logger.info "Removing agent #{agent} from the queue"
rescue Adhearsion::Call::ExpiredError
end
remove_call(call) click to toggle source

Remove a waiting call from the queue. Used if the caller hangs up or is otherwise removed. @param [Adhearsion::Call] call Caller to be removed from the queue

# File lib/electric_slide/call_queue.rb, line 266
def remove_call(call)
  ignoring_ended_calls do
    unless call[:electric_slide_connected_at]
      logger.info "Caller #{remote_party call} has abandoned the queue"
    end
  end
  @queue.delete call
end
return_agent(agent, new_presence = :available, address = nil) click to toggle source

Marks an agent as available to take a call. To be called after an agent completes a call and is ready to take the next call. @param [Agent] agent The {Agent} that is being returned to the queue @param [Symbol] new_presence The {Agent}‘s new presence @param [String, Optional] address The {Agent}’s address. Only specified if it has changed

# File lib/electric_slide/call_queue.rb, line 190
def return_agent(agent, new_presence = :available, address = nil)
  logger.debug "Returning #{agent} to the queue"

  return false unless get_agent(agent.id)

  agent.update_presence(new_presence)
  agent.address = address if address

  case agent.presence
  when :available
    bridged_agent_health_check agent

    @strategy << agent
    check_for_connections
  when :unavailable
    @strategy.delete agent
  end
  agent
end
return_agent!(*args) click to toggle source

Marks an agent as available to take a call. @see return_agent @raises [ElectricSlide::CallQueue::MissingAgentError] when the agent cannot be returned because they have been explicitly removed.

# File lib/electric_slide/call_queue.rb, line 213
def return_agent!(*args)
  return_agent(*args) || abort(MissingAgentError.new('Agent is not in the queue. Unable to return agent.'))
end
update(attrs) click to toggle source
# File lib/electric_slide/call_queue.rb, line 86
def update(attrs)
  attrs.each do |attr, value|
    setter = "#{attr}="
    send setter, value if respond_to?(setter)
  end unless attrs.nil?
end

Private Instance Methods

bridge_agent(agent, queued_call) click to toggle source
# File lib/electric_slide/call_queue.rb, line 420
def bridge_agent(agent, queued_call)
  # Stash caller ID to make log messages work even if calls end
  queued_caller_id = remote_party queued_call
  agent.call[:queued_call] = queued_call

  queue = current_actor
  agent.call.register_tmp_handler :event, Punchblock::Event::Unjoined do
    agent.callback :disconnect, queue, agent.call, queued_call
    ignoring_ended_calls { queued_call.hangup }
    ignoring_ended_calls { conditionally_return_agent agent if agent.call && agent.call.active? }
    agent.call[:queued_call] = nil if agent.call
  end

  queued_call.register_tmp_handler :event, Punchblock::Event::Joined do |event|
    queued_call[:electric_slide_connected_at] = event.timestamp
  end

  agent.callback :connect, current_actor, agent.call, queued_call

  agent.join queued_call if queued_call.active?
rescue *ENDED_CALL_EXCEPTIONS
  ignoring_ended_calls do
    if agent.call && agent.call.active?
      agent.callback :connection_failed, current_actor, agent.call, queued_call

      logger.info "Caller #{queued_caller_id} failed to connect to Agent #{agent.id} due to caller hangup"
      conditionally_return_agent agent, :auto
    end
  end

  ignoring_ended_calls do
    if queued_call.active?
      priority_enqueue queued_call
      logger.warn "Call failed to connect to Agent #{agent.id} due to agent hangup; reinserting caller #{queued_caller_id} into queue"
    end
  end
end
bridged_agent_health_check(agent) click to toggle source

@private

# File lib/electric_slide/call_queue.rb, line 459
def bridged_agent_health_check(agent)
  if agent.presence == :available && @connection_type == :bridge
    abort ArgumentError.new("Agent has no active call") unless agent.call && agent.call.active?
    unless agent.call[:electric_slide_callback_set]
      agent.call[:electric_slide_callback_set] = true
      queue = current_actor
      agent.call.on_end do
        agent.call = nil
        queue.return_agent agent, :unavailable
      end
    end
  end
end
call_agent(agent, queued_call) click to toggle source
# File lib/electric_slide/call_queue.rb, line 360
def call_agent(agent, queued_call)
  agent_call = Adhearsion::OutboundCall.new
  agent_call[:agent]  = agent
  agent_call[:queued_call] = queued_call

  agent.call = agent_call

  # Stash the caller ID so we don't have to try to get it from a dead call object later
  queued_caller_id = remote_party queued_call

  queue = current_actor

  # The call controller is actually run by #dial, here we skip joining if we do not have one
  dial_options = agent.dial_options_for(queue, queued_call)
  unless dial_options[:confirm]
    agent_call.on_answer { ignoring_ended_calls { agent_call.join queued_call.uri if queued_call.active? } }
  end

  # Disconnect agent if caller hangs up before agent answers
  queued_call.on_end { ignoring_ended_calls { agent_call.hangup } }

  agent_call.on_unjoined do
   ignoring_ended_calls { agent_call.hangup }
   ignoring_ended_calls { queued_call.hangup }
  end

  # Track whether the agent actually talks to the queued_call
  connected = false
  queued_call.register_tmp_handler :event, Punchblock::Event::Joined do |event|
    connected = true
    queued_call[:electric_slide_connected_at] = event.timestamp
  end

  agent_call.on_end do |end_event|
    # Ensure we don't return an agent that was removed or paused
    conditionally_return_agent agent

    agent.call = nil

    agent.callback :disconnect, queue, agent_call, queued_call

    unless connected
      if queued_call.active?
        ignoring_ended_calls { priority_enqueue queued_call }
        agent.callback :connection_failed, queue, agent_call, queued_call

        logger.warn "Call did not connect to agent! Agent #{agent.id} call ended with #{end_event.reason}; reinserting caller #{queued_caller_id} into queue"
      else
        logger.warn "Caller #{queued_caller_id} hung up before being connected to an agent."
      end
    end
  end

  agent.callback :connect, queue, agent_call, queued_call

  agent_call.execute_controller_or_router_on_answer dial_options.delete(:confirm), dial_options.delete(:confirm_metadata)

  agent_call.dial agent.address, dial_options
end
ignoring_ended_calls() { || ... } click to toggle source

@private

# File lib/electric_slide/call_queue.rb, line 354
def ignoring_ended_calls
  yield
rescue *ENDED_CALL_EXCEPTIONS
  # This actor may previously have been shut down due to the call ending
end
remote_party(call) click to toggle source

Get the caller ID of the remote party. If this is an OutboundCall, use Call#to Otherwise, use Call#from

# File lib/electric_slide/call_queue.rb, line 349
def remote_party(call)
  call.is_a?(Adhearsion::OutboundCall) ? call.to : call.from
end