electric_slide 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7b7f9f14186f1dee1e5d75e38e79f6c1c127522e
4
- data.tar.gz: 4f44af8a5750560e6f802227e36227142fba373f
3
+ metadata.gz: 018120f529e0824da4b7fb13d5ce713b53c16ec7
4
+ data.tar.gz: 7172bc885ad750962da497dd0d1afa560f5d5729
5
5
  SHA512:
6
- metadata.gz: 7d3f04f5f1ce98f47913ba04fed0da9cf827c9e1bed80e9e6731e0e22e371b6214e1e03550213a7f48abcf3259732b538ea75a71b8d65fc436311cd240d2747f
7
- data.tar.gz: 6cf73dd16a211f07e738e0cbd779e1a099e88786e57ec087763cfa58c1b77a84917c2e39020354fad302ee4a11de36a0907d002925da38a1276b9a571aafdd4a
6
+ metadata.gz: 8b3f3a9fd91292a2582f60c53f1d27a924b9ecb43d66013140f9c41ade3708fc2f6793a06cb3300bff0a4641116b1a9cef8afdcca84a120e3eff9fdf52043b1f
7
+ data.tar.gz: 8d9a3b56211a774348a0f7b953308415a69ae662e40355a63782786d9ce525350d9662ab4bec9be55edefb33bdab4fda8ff5d8248ba467125d6161ac2ba3dda6
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
4
+ - 2.2.1
5
+ - 2.2.2
6
+ - jruby-9.0.0.0
7
+ matrix:
8
+ allow_failures:
9
+ - rvm: ruby-head
10
+ sudo: false
@@ -0,0 +1,21 @@
1
+ # [develop](https://github.com/adhearsion/electric_slide)
2
+ * Trigger an agent "presence change" callback when the agent is removed from the queue
3
+ * Bugfix: Fix `NameError` exception when referencing namespaced constant `Adhearsion::Call::ExpiredError` in injected `Adhearsion::Call#active?` method
4
+ * Provide Electric Slide with a way to check if a queue with a given set of attributes is valid/can be instantiated before Electric Slide adds it to the supervision group. The supervisor will crash if its attempt to create the queue raises an exception.
5
+ * Add support for changing queue attributes
6
+ * API Breakage: Setting queue connection type to an invalid value will now raise an `ElectricSlide::CallQueue::InvalidConnectionType` exception instead of `ArgumentError`
7
+ * API Breakage: Setting queue agent return method to an invalid value will now raise an `ElectricSlide::CallQueue::InvalidRequeueMethod` exception instead of `ArgumentError`
8
+ * List created queues by name via `ElectricSlide.queues_by_name`
9
+ * Set `:agent` call variable on queued call when connecting calls
10
+ * API Breakage: Queues must now be Celluloid actors responding to the standard actor API. `ElectricSlide::CallQueue.work` is removed in favour of `.new`.
11
+ * API Breakage: Prevent an unqueued agent from being returned to the queue - this avoids calls after logging out
12
+ * API Breakage: An agent's presence should be :after_call if they are not automatically returned to being available
13
+ * API Breakage: Store queued/connected timestamps on calls
14
+ * API Breakage: Remove abandoned calls from the queue
15
+ * Set agent `#call` attribute to outbound call made to agent in :call mode
16
+ * Prevent an agent from being added to the queue more than once
17
+ * Added Travis CI configuration
18
+ * Lots more test coverage - still bad
19
+
20
+ # [0.2.0](https://github.com/adhearsion/electric_slide/compare/bb3b1b3e7f6d0926d0a9f462520e1f6d0c277adf...v0.2.0) - [2015-07-23](https://rubygems.org/gems/adhearsion/versions/0.2.0)
21
+ * ¯\\_(ツ)_/¯
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 The Adhearsion Foundation, Inc.
1
+ Copyright (c) 2011-2015 The Adhearsion Foundation, Inc.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
4
 
@@ -1,7 +1,7 @@
1
1
  Electric Slide - Simple Call Distribution for Adhearsion
2
2
  ====================================================================
3
3
 
4
- This library implements a simple FIFO (First-In, First-Out) call queue for Adhearsion.
4
+ This library implements a simple call queue for Adhearsion.
5
5
 
6
6
  To ensure proper operation, a few things are assumed:
7
7
 
@@ -63,7 +63,7 @@ end
63
63
  Adding an Agent to the Queue
64
64
  ----------------------------
65
65
 
66
- ElectricSlide expects to be given a objects that quack like an agent. You can use the built-in `ElectricSlide::Agent` class, or you can provide your own.
66
+ ElectricSlide expects to be instances of the `ElectricSlide::Agent` class. This is designed to be extended with custom functionality when necessary.
67
67
 
68
68
  To add an agent who will receive calls whenever a call is enqueued, do something like this:
69
69
 
@@ -72,7 +72,7 @@ agent = ElectricSlide::Agent.new id: 1, address: 'sip:agent1@example.com', prese
72
72
  ElectricSlide.get_queue(:my_queue).add_agent agent
73
73
  ```
74
74
 
75
- To inform the queue that the agent is no longer available you *must* use the ElectricSlide queue interface. /Do not attempt to alter agent objects directly!/
75
+ To inform the queue that the agent is no longer available you *must* use the ElectricSlide queue interface. **_Do not attempt to change agent objects directly!_**
76
76
 
77
77
  ```ruby
78
78
  ElectricSlide.update_agent 1, presence: offline
@@ -84,12 +84,21 @@ If it is more convenient, you may also pass `#update_agent` an Agent-like object
84
84
  options = {
85
85
  id: 1,
86
86
  address: 'sip:agent1@example.com',
87
- presence: offline
87
+ presence: :unavailable
88
88
  }
89
89
  agent = ElectricSlide::Agent.new options
90
90
  ElectricSlide.update_agent 1, agent
91
91
  ```
92
92
 
93
+ The possible presence states for an Agent are:
94
+
95
+ * `:available` - Waiting for a call
96
+ * `:on_call` - Currently connected to a call
97
+ * `:after_call` - In a quiet period after completing a call and before being made available again. This is only encountered with a manual agent return strategy.
98
+ * `:unavailable` - Agent is not available (on break, offline, etc)
99
+
100
+ Note that an `:unavailable` agent still counts as an agent in the queue, but will not be sent any calls. Make sure to remove agents, even unavailable ones, when agents sign out by using the `#remove_agent` method.
101
+
93
102
  Switching connection types
94
103
  --------------------------
95
104
 
@@ -124,8 +133,10 @@ Custom Agent Behavior
124
133
 
125
134
  If you need custom functionality to occur whenever an Agent is selected to take a call, you can use the callbacks on the Agent object:
126
135
 
127
- * `on_connect`
128
- * `on_disconnect`
136
+ * `on_connect`: Args: [Queue, Agent Call, Client Call] Called as the agent is being connected to the client call
137
+ * `on_disconnect`: Args: [Queue, Agent Call, Client Call] Called after the agent is disconnected from the client for any reason (eg. hangup)
138
+ * `connection_failed`: Args: [Queue, Agent Call, Client Call] Called when the agent fails to connect with the client for any reason (eg. no answer)
139
+ * `presence_change`: Args: [Queue, Agent Call, New Presence] Called after the agent's presence changes
129
140
 
130
141
  Confirmation Controllers
131
142
  ------------------------
@@ -151,3 +162,19 @@ call.join metadata[:caller] if confirm!
151
162
  ```
152
163
 
153
164
  where `confirm!` is your logic for deciding if you want the call to be connected or not. Hanging up during the confirmation controller or letting it finish without any action will result in the call being sent to the next agent.
165
+
166
+ Credits
167
+ -------
168
+
169
+ Electric Slide Copyright 2011-2015 Adhearsion Foundation Inc.
170
+ See the LICENSE file for more information.
171
+
172
+ Original Author [Ben Klang](https://github.com/bklang) - Mojo Lingo
173
+
174
+ Contributors:
175
+ * [Ben Langfeld](https://github.com/benlangfeld) - Mojo Lingo
176
+ * [Neil Decapia](https://github.com/neildecapia) - Mojo Lingo
177
+ * [Lloyd Hughes](https://github.com/system123) - Teleforge
178
+ * [Luca Pradovera](https://github.com/polysics) - Mojo Lingo
179
+
180
+ Also thanks to [Power Home Remodeling Group](http://powerhrg.com), [Teleforge](http://teleforge.co.za), and Atlanta Game Adventures for sponsoring development.
@@ -18,6 +18,7 @@ Gem::Specification.new do |s|
18
18
 
19
19
  s.has_rdoc = true
20
20
  s.homepage = "http://github.com/adhearsion/electric_slide"
21
+ s.license = "MIT"
21
22
  s.require_paths = ["lib"]
22
23
  s.rubygems_version = "1.2.0"
23
24
  s.summary = "Automatic Call Distributor for Adhearsion"
@@ -25,7 +26,8 @@ Gem::Specification.new do |s|
25
26
  s.add_runtime_dependency 'adhearsion'
26
27
  s.add_runtime_dependency 'countdownlatch'
27
28
  s.add_runtime_dependency 'activesupport'
28
- s.add_development_dependency 'rspec', ['>= 2.5.0']
29
+ s.add_development_dependency 'rspec', ['~> 3.0']
30
+ s.add_development_dependency 'timecop'
29
31
  s.add_development_dependency 'ci_reporter'
30
32
  s.add_development_dependency 'guard'
31
33
  s.add_development_dependency 'guard-rspec'
@@ -1,68 +1,68 @@
1
1
  # encoding: utf-8
2
2
  require 'celluloid'
3
- require 'singleton'
4
3
 
5
4
  require 'adhearsion/version'
6
5
 
7
6
  if Gem::Version.new(Adhearsion::VERSION) < Gem::Version.new('3.0.0')
8
7
  # Backport https://github.com/adhearsion/adhearsion/commit/8c6855612c70dd822fb4e4c2006d1fdc9d05fe23 to avoid confusion around dead calls
9
8
  require 'adhearsion/call'
10
- class Adhearsion::Call::ActorProxy < Celluloid::ActorProxy
11
- def active?
12
- alive? && super
13
- rescue Adhearsion::Call::ExpiredError
14
- false
9
+ class Adhearsion::Call
10
+ class ActorProxy
11
+ def active?
12
+ alive? && super
13
+ rescue ExpiredError
14
+ false
15
+ end
15
16
  end
16
17
  end
17
18
  end
18
19
 
19
20
  %w(
21
+ agent
20
22
  call_queue
21
23
  plugin
22
24
  ).each { |f| require "electric_slide/#{f}" }
23
25
 
24
26
  class ElectricSlide
25
- include Singleton
27
+ class Supervisor < Celluloid::SupervisionGroup
28
+ def [](name)
29
+ @registry[name]
30
+ end
26
31
 
27
- def initialize
28
- @mutex = Mutex.new
29
- @queues = {}
32
+ def names
33
+ @registry.names
34
+ end
30
35
  end
31
36
 
32
- def create(name, queue_class = nil, *args)
33
- fail "Queue with name #{name} already exists!" if @queues.key? name
37
+ @supervisor = Supervisor.run!(Celluloid::Registry.new)
34
38
 
35
- queue_class ||= CallQueue
36
- @queues[name] = queue_class.work *args
37
- # Return the queue instance or current actor
38
- get_queue name
39
+ def self.queues_by_name
40
+ @supervisor.names.inject({}) do |queues, name|
41
+ queues[name] = get_queue(name)
42
+ queues
43
+ end
39
44
  end
40
45
 
41
- def get_queue!(name)
42
- fail "Queue #{name} not found!" unless @queues.key?(name)
43
- get_queue name
44
- end
46
+ def self.create(name, queue_class = nil, *args)
47
+ fail "Queue with name #{name} already exists!" if get_queue(name)
45
48
 
46
- def get_queue(name)
47
- queue = @queues[name]
48
- if queue.respond_to? :actors
49
- # In case we have a Celluloid supervision group, get the current actor
50
- queue.actors.first
51
- else
52
- queue
49
+ queue_class ||= CallQueue
50
+ if !queue_class.respond_to?(:valid_with?) || queue_class.valid_with?(*args)
51
+ @supervisor.supervise_as name, (queue_class || CallQueue), *args
52
+ get_queue name
53
53
  end
54
54
  end
55
55
 
56
- def shutdown_queue(name)
57
- queue = get_queue name
58
- queue.terminate
59
- @queues.delete name
56
+ def self.get_queue!(name)
57
+ get_queue(name) || fail("Queue #{name} not found!")
60
58
  end
61
59
 
62
- def self.method_missing(method, *args, &block)
63
- @@mutex ||= Mutex.new
64
- @@mutex.synchronize do
65
- instance.send method, *args, &block
66
- end
60
+ def self.get_queue(name)
61
+ @supervisor[name]
62
+ end
63
+
64
+ def self.shutdown_queue(name)
65
+ queue = get_queue name
66
+ queue.terminate if queue
67
67
  end
68
68
  end
@@ -1,48 +1,71 @@
1
1
  # encoding: utf-8
2
- class ElectricSlide::Agent
3
- attr_accessor :id, :address, :presence, :call, :connect_callback, :disconnect_callback
4
-
5
- # @param [Hash] opts Agent parameters
6
- # @option opts [String] :id The Agent's ID
7
- # @option opts [String] :address The Agent's contact address
8
- # @option opts [Symbol] :presence The Agent's current presence. Must be one of :available, :on_call, :away, :offline
9
- def initialize(opts = {})
10
- @id = opts[:id]
11
- @address = opts[:address]
12
- @presence = opts[:presence]
13
- end
14
2
 
15
- def callback(type, *args)
16
- callback = self.class.instance_variable_get "@#{type}_callback"
17
- instance_exec *args, &callback if callback && callback.respond_to?(:call)
18
- end
3
+ class ElectricSlide
4
+ class Agent
5
+ attr_accessor :id, :address, :presence, :call, :queue
19
6
 
7
+ # @param [Hash] opts Agent parameters
8
+ # @option opts [String] :id The Agent's ID
9
+ # @option opts [String] :address The Agent's contact address
10
+ # @option opts [Symbol] :presence The Agent's current presence. Must be one of :available, :on_call, :after_call, :unavailable
11
+ def initialize(opts = {})
12
+ @id = opts[:id]
13
+ @address = opts[:address]
14
+ @presence = opts[:presence] || :available
15
+ end
20
16
 
21
- # Provide a block to be called when this agent is connected to a caller
22
- # The block will be passed the queue, the agent call and the client call
23
- def self.on_connect(&block)
24
- @connect_callback = block
25
- end
17
+ def presence=(new_presence)
18
+ old_presence = @presence
19
+ @presence = new_presence
20
+ callback :presence_change, queue, @call, new_presence, old_presence
21
+ end
26
22
 
27
- # Provide a block to be called when this agent is disconnected to a caller
28
- # The block will be passed the queue, the agent call and the client call
29
- def self.on_disconnect(&block)
30
- @disconnect_callback = block
31
- end
23
+ def callback(type, *args)
24
+ callback = self.class.instance_variable_get "@#{type}_callback"
25
+ instance_exec *args, &callback if callback && callback.respond_to?(:call)
26
+ end
32
27
 
33
- # Called to provide options for calling this agent that are passed to #dial
34
- def dial_options_for(queue, queued_call)
35
- {}
36
- end
28
+ # Provide a block to be called when this agent is connected to a caller
29
+ # The block will be passed the queue, the agent call and the client call
30
+ def self.on_connect(&block)
31
+ @connect_callback = block
32
+ end
37
33
 
38
- def join(queued_call)
39
- # For use in queues that need bridge connections
40
- @call.join queued_call
41
- end
34
+ # Provide a block to be called when this agent is disconnected to a caller
35
+ # The block will be passed the queue, the agent call and the client call
36
+ def self.on_disconnect(&block)
37
+ @disconnect_callback = block
38
+ end
42
39
 
43
- # FIXME: Use delegator?
44
- def from
45
- @call.from
40
+ # Provide a block to be called when this agent's presence changes
41
+ # The block will be passed the queue, the agent call, and the new presence
42
+ def self.on_presence_change(&block)
43
+ @presence_change_callback = block
44
+ end
45
+
46
+ # Provide a block to be called when the agent connection to the callee fails
47
+ # The block will be passed the queue, the agent call and the client call
48
+ def self.on_connection_failed(&block)
49
+ @connection_failed_callback = block
50
+ end
51
+
52
+ def on_call?
53
+ @presence == :on_call
54
+ end
55
+
56
+ # Called to provide options for calling this agent that are passed to #dial
57
+ def dial_options_for(queue, queued_call)
58
+ {}
59
+ end
60
+
61
+ def join(queued_call)
62
+ # For use in queues that need bridge connections
63
+ @call.join queued_call
64
+ end
65
+
66
+ # FIXME: Use delegator?
67
+ def from
68
+ @call.from
69
+ end
46
70
  end
47
71
  end
48
-
@@ -20,7 +20,7 @@ class ElectricSlide
20
20
  { total: @free_agents.count }
21
21
  end
22
22
 
23
- # Assigns the first available agent, marking the agent :busy
23
+ # Assigns the first available agent, marking the agent :on_call
24
24
  # @return {Agent}
25
25
  def checkout_agent
26
26
  @free_agents.shift
@@ -6,6 +6,7 @@ require 'electric_slide/agent_strategy/longest_idle'
6
6
  class ElectricSlide
7
7
  class CallQueue
8
8
  include Celluloid
9
+
9
10
  ENDED_CALL_EXCEPTIONS = [
10
11
  Adhearsion::Call::Hangup,
11
12
  Adhearsion::Call::ExpiredError,
@@ -24,23 +25,90 @@ class ElectricSlide
24
25
  :manual,
25
26
  ].freeze
26
27
 
27
- def self.work(*args)
28
- self.supervise *args
28
+ Error = Class.new(StandardError)
29
+
30
+ MissingAgentError = Class.new(Error)
31
+ DuplicateAgentError = Class.new(Error)
32
+
33
+ class InvalidConnectionType < Error
34
+ def message
35
+ "Invalid connection type; must be one of #{CONNECTION_TYPES.join ','}"
36
+ end
29
37
  end
30
38
 
31
- def initialize(opts = {})
32
- agent_strategy = opts[:agent_strategy] || AgentStrategy::LongestIdle
33
- @connection_type = opts[:connection_type] || :call
34
- @agent_return_method = opts[:agent_return_method] || :auto
39
+ class InvalidRequeueMethod < Error
40
+ def message
41
+ "Invalid requeue method; must be one of #{AGENT_RETURN_METHODS.join ','}"
42
+ end
43
+ end
44
+
45
+ attr_reader :agent_strategy, :connection_type, :agent_return_method
46
+
47
+ def self.valid_with?(attrs = {})
48
+ return false unless Hash === attrs
35
49
 
36
- raise ArgumentError, "Invalid connection type; must be one of #{CONNECTION_TYPES.join ','}" unless CONNECTION_TYPES.include? @connection_type
37
- raise ArgumentError, "Invalid requeue method; must be one of #{AGENT_RETURN_METHODS.join ','}" unless AGENT_RETURN_METHODS.include? @agent_return_method
50
+ if agent_strategy = attrs[:agent_strategy]
51
+ begin
52
+ agent_strategy.new
53
+ rescue Exception
54
+ return false
55
+ end
56
+ end
57
+ if connection_type = attrs[:connection_type]
58
+ return false unless valid_connection_type? connection_type
59
+ end
60
+ if agent_return_method = attrs[:agent_return_method]
61
+ return false unless valid_agent_return_method? agent_return_method
62
+ end
38
63
 
39
- @free_agents = [] # Needed to keep track of waiting order
64
+ true
65
+ end
66
+
67
+ def self.valid_connection_type?(connection_type)
68
+ CONNECTION_TYPES.include? connection_type
69
+ end
70
+
71
+ def self.valid_agent_return_method?(agent_return_method)
72
+ AGENT_RETURN_METHODS.include? agent_return_method
73
+ end
74
+
75
+ def initialize(opts = {})
40
76
  @agents = [] # Needed to keep track of global list of agents
41
77
  @queue = [] # Calls waiting for an agent
42
78
 
43
- @strategy = agent_strategy.new
79
+ update(
80
+ agent_strategy: opts[:agent_strategy] || AgentStrategy::LongestIdle,
81
+ connection_type: opts[:connection_type] || :call,
82
+ agent_return_method: opts[:agent_return_method] || :auto
83
+ )
84
+ end
85
+
86
+ def update(attrs)
87
+ attrs.each do |attr, value|
88
+ setter = "#{attr}="
89
+ send setter, value if respond_to?(setter)
90
+ end unless attrs.nil?
91
+ end
92
+
93
+ def agent_strategy=(new_agent_strategy)
94
+ @agent_strategy = new_agent_strategy
95
+
96
+ @strategy = @agent_strategy.new
97
+ @agents.each do |agent|
98
+ return_agent agent, agent.presence
99
+ end
100
+
101
+ @agent_strategy
102
+ end
103
+
104
+ def connection_type=(new_connection_type)
105
+ abort InvalidConnectionType.new unless CallQueue.valid_connection_type? new_connection_type
106
+ @connection_type = new_connection_type
107
+ end
108
+
109
+ def agent_return_method=(new_agent_return_method)
110
+ abort InvalidRequeueMethod.new unless CallQueue.valid_agent_return_method? new_agent_return_method
111
+ @agent_return_method = new_agent_return_method
44
112
  end
45
113
 
46
114
  # Checks whether an agent is available to take a call
@@ -58,11 +126,13 @@ class ElectricSlide
58
126
  @strategy.available_agent_summary
59
127
  end
60
128
 
61
- # Assigns the first available agent, marking the agent :busy
129
+ # Assigns the first available agent, marking the agent :on_call
62
130
  # @return {Agent}
63
131
  def checkout_agent
64
132
  agent = @strategy.checkout_agent
65
- agent.presence = :busy
133
+ if agent
134
+ agent.presence = :on_call
135
+ end
66
136
  agent
67
137
  end
68
138
 
@@ -87,46 +157,68 @@ class ElectricSlide
87
157
 
88
158
  # Registers an agent to the queue
89
159
  # @param [Agent] agent The agent to be added to the queue
160
+ # @raise ArgumentError if the agent is malformed
161
+ # @raise DuplicateAgentError if this agent ID already exists
162
+ # @see #update_agent
90
163
  def add_agent(agent)
91
164
  abort ArgumentError.new("#add_agent called with nil object") if agent.nil?
165
+ abort DuplicateAgentError.new("Agent is already in the queue") if get_agent(agent.id)
166
+
167
+ agent.queue = self
168
+
92
169
  case @connection_type
93
170
  when :call
94
171
  abort ArgumentError.new("Agent has no callable address") unless agent.address
95
172
  when :bridge
96
- abort ArgumentError.new("Agent has no active call") unless agent.call && agent.call.active?
97
- unless agent.call[:electric_slide_callback_set]
98
- agent.call.on_end { remove_agent agent }
99
- agent.call[:electric_slide_callback_set] = true
100
- end
173
+ bridged_agent_health_check agent
101
174
  end
102
175
 
103
176
  logger.info "Adding agent #{agent} to the queue"
104
- @agents << agent unless @agents.include? agent
177
+ @agents << agent
105
178
  @strategy << agent if agent.presence == :available
179
+ # Fake the presence callback since this is a new agent
180
+ agent.callback :presence_change, self, agent.call, agent.presence, :unavailable
181
+
106
182
  check_for_connections
107
183
  end
108
184
 
109
185
  # Marks an agent as available to take a call. To be called after an agent completes a call
110
186
  # and is ready to take the next call.
111
187
  # @param [Agent] agent The {Agent} that is being returned to the queue
112
- # @param [Symbol] status The {Agent}'s new status
188
+ # @param [Symbol] new_presence The {Agent}'s new presence
113
189
  # @param [String, Optional] address The {Agent}'s address. Only specified if it has changed
114
- def return_agent(agent, status = :available, address = nil)
190
+ def return_agent(agent, new_presence = :available, address = nil)
115
191
  logger.debug "Returning #{agent} to the queue"
116
- agent.presence = status
192
+
193
+ return false unless get_agent(agent.id)
194
+
195
+ agent.presence = new_presence
117
196
  agent.address = address if address
118
197
 
119
- if agent.presence == :available
198
+ case agent.presence
199
+ when :available
200
+ bridged_agent_health_check agent
201
+
120
202
  @strategy << agent
121
203
  check_for_connections
204
+ when :unavailable
205
+ @strategy.delete agent
122
206
  end
123
207
  agent
124
208
  end
125
209
 
210
+ # Marks an agent as available to take a call.
211
+ # @see #return_agent
212
+ # @raises [ElectricSlide::CallQueue::MissingAgentError] when the agent cannot be returned because they have been explicitly removed.
213
+ def return_agent!(*args)
214
+ return_agent(*args) || abort(MissingAgentError.new('Agent is not in the queue. Unable to return agent.'))
215
+ end
216
+
126
217
  # Removes an agent from the queue entirely
127
218
  # @param [Agent] agent The {Agent} to be removed from the queue
128
219
  # @return [Agent, Nil] The Agent object if removed, Nil otherwise
129
220
  def remove_agent(agent)
221
+ agent.presence = :unavailable
130
222
  @strategy.delete agent
131
223
  @agents.delete agent
132
224
  logger.info "Removing agent #{agent} from the queue"
@@ -143,8 +235,13 @@ class ElectricSlide
143
235
  # to an agent, but the connection fails because the agent is not available.
144
236
  # @param [Adhearsion::Call] call Caller to be added to the queue
145
237
  def priority_enqueue(call)
146
- # Don't reset the enqueue time in case this is a re-insert on agent failure
147
- call[:enqueue_time] ||= Time.now
238
+ # In case this is a re-insert on agent failure...
239
+ # ... reset `:agent` call variable
240
+ call[:agent] = nil
241
+ # ... set, but don't reset, the enqueue time
242
+ call[:electric_slide_enqueued_at] ||= DateTime.now
243
+
244
+ call.on_end { remove_call call }
148
245
  @queue.unshift call
149
246
 
150
247
  check_for_connections
@@ -155,7 +252,8 @@ class ElectricSlide
155
252
  def enqueue(call)
156
253
  ignoring_ended_calls do
157
254
  logger.info "Adding call from #{remote_party call} to the queue"
158
- call[:enqueue_time] = Time.now
255
+ call[:electric_slide_enqueued_at] = DateTime.now
256
+ call.on_end { remove_call call }
159
257
  @queue << call unless @queue.include? call
160
258
 
161
259
  check_for_connections
@@ -164,8 +262,12 @@ class ElectricSlide
164
262
 
165
263
  # Remove a waiting call from the queue. Used if the caller hangs up or is otherwise removed.
166
264
  # @param [Adhearsion::Call] call Caller to be removed from the queue
167
- def abandon(call)
168
- ignoring_ended_calls { logger.info "Caller #{remote_party call} has abandoned the queue" }
265
+ def remove_call(call)
266
+ ignoring_ended_calls do
267
+ unless call[:electric_slide_connected_at]
268
+ logger.info "Caller #{remote_party call} has abandoned the queue"
269
+ end
270
+ end
169
271
  @queue.delete call
170
272
  end
171
273
 
@@ -178,12 +280,14 @@ class ElectricSlide
178
280
  return_agent agent
179
281
  end
180
282
 
283
+ queued_call[:agent] = agent
284
+
181
285
  logger.info "Connecting #{agent} with #{remote_party queued_call}"
182
286
  case @connection_type
183
287
  when :call
184
288
  call_agent agent, queued_call
185
289
  when :bridge
186
- unless agent.call.active?
290
+ unless agent.call && agent.call.active?
187
291
  logger.warn "Inactive agent call found in #connect, returning caller to queue"
188
292
  priority_enqueue queued_call
189
293
  end
@@ -199,6 +303,8 @@ class ElectricSlide
199
303
  ignoring_ended_calls do
200
304
  if agent.call && agent.call.active?
201
305
  logger.warn "Dead call exception in #connect but agent call still alive, reinserting into queue"
306
+ agent.callback :connection_failed, self, agent.call, queued_call
307
+
202
308
  return_agent agent
203
309
  end
204
310
  end
@@ -207,11 +313,12 @@ class ElectricSlide
207
313
  def conditionally_return_agent(agent, return_method = @agent_return_method)
208
314
  raise ArgumentError, "Invalid requeue method; must be one of #{AGENT_RETURN_METHODS.join ','}" unless AGENT_RETURN_METHODS.include? return_method
209
315
 
210
- if agent && @agents.include?(agent) && agent.presence == :busy && return_method == :auto
316
+ if agent && @agents.include?(agent) && agent.on_call? && return_method == :auto
211
317
  logger.info "Returning agent #{agent.id} to queue"
212
318
  return_agent agent
213
319
  else
214
320
  logger.debug "Not returning agent #{agent.inspect} to the queue"
321
+ return_agent agent, :after_call
215
322
  end
216
323
  end
217
324
 
@@ -234,6 +341,7 @@ class ElectricSlide
234
341
  end
235
342
 
236
343
  private
344
+
237
345
  # Get the caller ID of the remote party.
238
346
  # If this is an OutboundCall, use Call#to
239
347
  # Otherwise, use Call#from
@@ -241,7 +349,6 @@ class ElectricSlide
241
349
  call.is_a?(Adhearsion::OutboundCall) ? call.to : call.from
242
350
  end
243
351
 
244
-
245
352
  # @private
246
353
  def ignoring_ended_calls
247
354
  yield
@@ -254,6 +361,8 @@ class ElectricSlide
254
361
  agent_call[:agent] = agent
255
362
  agent_call[:queued_call] = queued_call
256
363
 
364
+ agent.call = agent_call
365
+
257
366
  # Stash the caller ID so we don't have to try to get it from a dead call object later
258
367
  queued_caller_id = remote_party queued_call
259
368
 
@@ -273,17 +382,24 @@ class ElectricSlide
273
382
 
274
383
  # Track whether the agent actually talks to the queued_call
275
384
  connected = false
276
- queued_call.on_joined { connected = true }
385
+ queued_call.register_tmp_handler :event, Punchblock::Event::Joined do |event|
386
+ connected = true
387
+ queued_call[:electric_slide_connected_at] = event.timestamp
388
+ end
277
389
 
278
390
  agent_call.on_end do |end_event|
279
391
  # Ensure we don't return an agent that was removed or paused
280
392
  conditionally_return_agent agent
281
393
 
394
+ agent.call = nil
395
+
282
396
  agent.callback :disconnect, self, agent_call, queued_call
283
397
 
284
398
  unless connected
285
- if queued_call.alive? && queued_call.active?
399
+ if queued_call.active?
286
400
  ignoring_ended_calls { priority_enqueue queued_call }
401
+ agent.callback :connection_failed, self, agent_call, queued_call
402
+
287
403
  logger.warn "Call did not connect to agent! Agent #{agent.id} call ended with #{end_event.reason}; reinserting caller #{queued_caller_id} into queue"
288
404
  else
289
405
  logger.warn "Caller #{queued_caller_id} hung up before being connected to an agent."
@@ -306,8 +422,12 @@ class ElectricSlide
306
422
  agent.call.register_tmp_handler :event, Punchblock::Event::Unjoined do
307
423
  agent.callback :disconnect, self, agent.call, queued_call
308
424
  ignoring_ended_calls { queued_call.hangup }
309
- ignoring_ended_calls { conditionally_return_agent agent if agent.call.active? }
310
- agent.call[:queued_call] = nil
425
+ ignoring_ended_calls { conditionally_return_agent agent if agent.call && agent.call.active? }
426
+ agent.call[:queued_call] = nil if agent.call
427
+ end
428
+
429
+ queued_call.register_tmp_handler :event, Punchblock::Event::Joined do |event|
430
+ queued_call[:electric_slide_connected_at] = event.timestamp
311
431
  end
312
432
 
313
433
  agent.callback :connect, self, agent.call, queued_call
@@ -315,12 +435,11 @@ class ElectricSlide
315
435
  agent.join queued_call if queued_call.active?
316
436
  rescue *ENDED_CALL_EXCEPTIONS
317
437
  ignoring_ended_calls do
318
- if agent.call.active?
438
+ if agent.call && agent.call.active?
439
+ agent.callback :connection_failed, self, agent.call, queued_call
440
+
319
441
  logger.info "Caller #{queued_caller_id} failed to connect to Agent #{agent.id} due to caller hangup"
320
442
  conditionally_return_agent agent, :auto
321
- else
322
- # Agent's call has ended, so remove him from the queue
323
- remove_agent agent
324
443
  end
325
444
  end
326
445
 
@@ -331,5 +450,20 @@ class ElectricSlide
331
450
  end
332
451
  end
333
452
  end
453
+
454
+ # @private
455
+ def bridged_agent_health_check(agent)
456
+ if agent.presence == :available && @connection_type == :bridge
457
+ abort ArgumentError.new("Agent has no active call") unless agent.call && agent.call.active?
458
+ unless agent.call[:electric_slide_callback_set]
459
+ agent.call[:electric_slide_callback_set] = true
460
+ queue = self
461
+ agent.call.on_end do
462
+ agent.call = nil
463
+ queue.return_agent agent, :unavailable
464
+ end
465
+ end
466
+ end
467
+ end
334
468
  end
335
469
  end