electric_slide 0.0.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/Gemfile +1 -1
- data/Guardfile +9 -0
- data/README.markdown +151 -1
- data/Rakefile +2 -3
- data/electric_slide.gemspec +6 -2
- data/lib/electric_slide.rb +45 -42
- data/lib/electric_slide/agent.rb +48 -0
- data/lib/electric_slide/agent_strategy/fixed_priority.rb +55 -0
- data/lib/electric_slide/agent_strategy/longest_idle.rb +39 -0
- data/lib/electric_slide/call_queue.rb +335 -0
- data/lib/electric_slide/plugin.rb +10 -0
- data/lib/electric_slide/version.rb +4 -0
- data/spec/electric_slide/agent_spec.rb +23 -0
- data/spec/electric_slide/agent_strategy/fixed_priority_spec.rb +43 -0
- data/spec/electric_slide/call_queue_spec.rb +42 -0
- data/spec/electric_slide_spec.rb +40 -0
- data/spec/spec_helper.rb +4 -11
- metadata +51 -34
- data/lib/electric_slide/queue_strategy.rb +0 -18
- data/lib/electric_slide/queued_call.rb +0 -24
- data/lib/electric_slide/round_robin.rb +0 -46
- data/lib/electric_slide/round_robin_meetme.rb +0 -34
- data/spec/electric_slide/queue_strategy_spec.rb +0 -16
- data/spec/electric_slide/queued_call_spec.rb +0 -42
- data/spec/electric_slide/round_robin_spec.rb +0 -87
@@ -0,0 +1,335 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# The default agent strategy
|
4
|
+
require 'electric_slide/agent_strategy/longest_idle'
|
5
|
+
|
6
|
+
class ElectricSlide
|
7
|
+
class CallQueue
|
8
|
+
include Celluloid
|
9
|
+
ENDED_CALL_EXCEPTIONS = [
|
10
|
+
Adhearsion::Call::Hangup,
|
11
|
+
Adhearsion::Call::ExpiredError,
|
12
|
+
Adhearsion::Call::CommandTimeout,
|
13
|
+
Celluloid::DeadActorError,
|
14
|
+
Punchblock::ProtocolError
|
15
|
+
]
|
16
|
+
|
17
|
+
CONNECTION_TYPES = [
|
18
|
+
:call,
|
19
|
+
:bridge,
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
AGENT_RETURN_METHODS = [
|
23
|
+
:auto,
|
24
|
+
:manual,
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
def self.work(*args)
|
28
|
+
self.supervise *args
|
29
|
+
end
|
30
|
+
|
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
|
35
|
+
|
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
|
38
|
+
|
39
|
+
@free_agents = [] # Needed to keep track of waiting order
|
40
|
+
@agents = [] # Needed to keep track of global list of agents
|
41
|
+
@queue = [] # Calls waiting for an agent
|
42
|
+
|
43
|
+
@strategy = agent_strategy.new
|
44
|
+
end
|
45
|
+
|
46
|
+
# Checks whether an agent is available to take a call
|
47
|
+
# @return [Boolean] True if an agent is available
|
48
|
+
def agent_available?
|
49
|
+
@strategy.agent_available?
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns information about the number of available agents
|
53
|
+
# The data returned depends on the AgentStrategy in use.
|
54
|
+
# The data will always include a :total count of the agents available
|
55
|
+
# @return [Hash] Summary information about agents available, depending on strategy
|
56
|
+
def available_agent_summary
|
57
|
+
# TODO: Make this a delegator?
|
58
|
+
@strategy.available_agent_summary
|
59
|
+
end
|
60
|
+
|
61
|
+
# Assigns the first available agent, marking the agent :busy
|
62
|
+
# @return {Agent}
|
63
|
+
def checkout_agent
|
64
|
+
agent = @strategy.checkout_agent
|
65
|
+
agent.presence = :busy
|
66
|
+
agent
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a copy of the set of agents that are known to the queue
|
70
|
+
# @return [Array] Array of {Agent} objects
|
71
|
+
def get_agents
|
72
|
+
@agents.dup
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns a copy of the set of calls waiting to be answered that are known to the queue
|
76
|
+
# @return [Array] Array of Adhearsion::Call objects
|
77
|
+
def get_queued_calls
|
78
|
+
@queue.dup
|
79
|
+
end
|
80
|
+
|
81
|
+
# Finds an agent known to the queue by that agent's ID
|
82
|
+
# @param [String] id The ID of the agent to locate
|
83
|
+
# @return [Agent, Nil] {Agent} object if found, Nil otherwise
|
84
|
+
def get_agent(id)
|
85
|
+
@agents.detect { |agent| agent.id == id }
|
86
|
+
end
|
87
|
+
|
88
|
+
# Registers an agent to the queue
|
89
|
+
# @param [Agent] agent The agent to be added to the queue
|
90
|
+
def add_agent(agent)
|
91
|
+
abort ArgumentError.new("#add_agent called with nil object") if agent.nil?
|
92
|
+
case @connection_type
|
93
|
+
when :call
|
94
|
+
abort ArgumentError.new("Agent has no callable address") unless agent.address
|
95
|
+
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
|
101
|
+
end
|
102
|
+
|
103
|
+
logger.info "Adding agent #{agent} to the queue"
|
104
|
+
@agents << agent unless @agents.include? agent
|
105
|
+
@strategy << agent if agent.presence == :available
|
106
|
+
check_for_connections
|
107
|
+
end
|
108
|
+
|
109
|
+
# Marks an agent as available to take a call. To be called after an agent completes a call
|
110
|
+
# and is ready to take the next call.
|
111
|
+
# @param [Agent] agent The {Agent} that is being returned to the queue
|
112
|
+
# @param [Symbol] status The {Agent}'s new status
|
113
|
+
# @param [String, Optional] address The {Agent}'s address. Only specified if it has changed
|
114
|
+
def return_agent(agent, status = :available, address = nil)
|
115
|
+
logger.debug "Returning #{agent} to the queue"
|
116
|
+
agent.presence = status
|
117
|
+
agent.address = address if address
|
118
|
+
|
119
|
+
if agent.presence == :available
|
120
|
+
@strategy << agent
|
121
|
+
check_for_connections
|
122
|
+
end
|
123
|
+
agent
|
124
|
+
end
|
125
|
+
|
126
|
+
# Removes an agent from the queue entirely
|
127
|
+
# @param [Agent] agent The {Agent} to be removed from the queue
|
128
|
+
# @return [Agent, Nil] The Agent object if removed, Nil otherwise
|
129
|
+
def remove_agent(agent)
|
130
|
+
@strategy.delete agent
|
131
|
+
@agents.delete agent
|
132
|
+
logger.info "Removing agent #{agent} from the queue"
|
133
|
+
rescue Adhearsion::Call::ExpiredError
|
134
|
+
end
|
135
|
+
|
136
|
+
# Checks to see if any callers are waiting for an agent and attempts to connect them to
|
137
|
+
# an available agent
|
138
|
+
def check_for_connections
|
139
|
+
connect checkout_agent, get_next_caller while call_waiting? && agent_available?
|
140
|
+
end
|
141
|
+
|
142
|
+
# Add a call to the head of the queue. Among other reasons, this is used when a caller is sent
|
143
|
+
# to an agent, but the connection fails because the agent is not available.
|
144
|
+
# @param [Adhearsion::Call] call Caller to be added to the queue
|
145
|
+
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
|
148
|
+
@queue.unshift call
|
149
|
+
|
150
|
+
check_for_connections
|
151
|
+
end
|
152
|
+
|
153
|
+
# Add a call to the end of the queue, the normal FIFO queue behavior
|
154
|
+
# @param [Adhearsion::Call] call Caller to be added to the queue
|
155
|
+
def enqueue(call)
|
156
|
+
ignoring_ended_calls do
|
157
|
+
logger.info "Adding call from #{remote_party call} to the queue"
|
158
|
+
call[:enqueue_time] = Time.now
|
159
|
+
@queue << call unless @queue.include? call
|
160
|
+
|
161
|
+
check_for_connections
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Remove a waiting call from the queue. Used if the caller hangs up or is otherwise removed.
|
166
|
+
# @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" }
|
169
|
+
@queue.delete call
|
170
|
+
end
|
171
|
+
|
172
|
+
# Connect an {Agent} to a caller
|
173
|
+
# @param [Agent] agent Agent to be connected
|
174
|
+
# @param [Adhearsion::Call] call Caller to be connected
|
175
|
+
def connect(agent, queued_call)
|
176
|
+
unless queued_call.active?
|
177
|
+
logger.warn "Inactive queued call found in #connect"
|
178
|
+
return_agent agent
|
179
|
+
end
|
180
|
+
|
181
|
+
logger.info "Connecting #{agent} with #{remote_party queued_call}"
|
182
|
+
case @connection_type
|
183
|
+
when :call
|
184
|
+
call_agent agent, queued_call
|
185
|
+
when :bridge
|
186
|
+
unless agent.call.active?
|
187
|
+
logger.warn "Inactive agent call found in #connect, returning caller to queue"
|
188
|
+
priority_enqueue queued_call
|
189
|
+
end
|
190
|
+
bridge_agent agent, queued_call
|
191
|
+
end
|
192
|
+
rescue *ENDED_CALL_EXCEPTIONS
|
193
|
+
ignoring_ended_calls do
|
194
|
+
if queued_call.active?
|
195
|
+
logger.warn "Dead call exception in #connect but queued_call still alive, reinserting into queue"
|
196
|
+
priority_enqueue queued_call
|
197
|
+
end
|
198
|
+
end
|
199
|
+
ignoring_ended_calls do
|
200
|
+
if agent.call && agent.call.active?
|
201
|
+
logger.warn "Dead call exception in #connect but agent call still alive, reinserting into queue"
|
202
|
+
return_agent agent
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def conditionally_return_agent(agent, return_method = @agent_return_method)
|
208
|
+
raise ArgumentError, "Invalid requeue method; must be one of #{AGENT_RETURN_METHODS.join ','}" unless AGENT_RETURN_METHODS.include? return_method
|
209
|
+
|
210
|
+
if agent && @agents.include?(agent) && agent.presence == :busy && return_method == :auto
|
211
|
+
logger.info "Returning agent #{agent.id} to queue"
|
212
|
+
return_agent agent
|
213
|
+
else
|
214
|
+
logger.debug "Not returning agent #{agent.inspect} to the queue"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Returns the next waiting caller
|
219
|
+
# @return [Adhearsion::Call] The next waiting caller
|
220
|
+
def get_next_caller
|
221
|
+
@queue.shift
|
222
|
+
end
|
223
|
+
|
224
|
+
# Checks whether any callers are waiting
|
225
|
+
# @return [Boolean] True if a caller is waiting
|
226
|
+
def call_waiting?
|
227
|
+
@queue.length > 0
|
228
|
+
end
|
229
|
+
|
230
|
+
# Returns the number of callers waiting in the queue
|
231
|
+
# @return [Fixnum]
|
232
|
+
def calls_waiting
|
233
|
+
@queue.length
|
234
|
+
end
|
235
|
+
|
236
|
+
private
|
237
|
+
# Get the caller ID of the remote party.
|
238
|
+
# If this is an OutboundCall, use Call#to
|
239
|
+
# Otherwise, use Call#from
|
240
|
+
def remote_party(call)
|
241
|
+
call.is_a?(Adhearsion::OutboundCall) ? call.to : call.from
|
242
|
+
end
|
243
|
+
|
244
|
+
|
245
|
+
# @private
|
246
|
+
def ignoring_ended_calls
|
247
|
+
yield
|
248
|
+
rescue *ENDED_CALL_EXCEPTIONS
|
249
|
+
# This actor may previously have been shut down due to the call ending
|
250
|
+
end
|
251
|
+
|
252
|
+
def call_agent(agent, queued_call)
|
253
|
+
agent_call = Adhearsion::OutboundCall.new
|
254
|
+
agent_call[:agent] = agent
|
255
|
+
agent_call[:queued_call] = queued_call
|
256
|
+
|
257
|
+
# Stash the caller ID so we don't have to try to get it from a dead call object later
|
258
|
+
queued_caller_id = remote_party queued_call
|
259
|
+
|
260
|
+
# The call controller is actually run by #dial, here we skip joining if we do not have one
|
261
|
+
dial_options = agent.dial_options_for(self, queued_call)
|
262
|
+
unless dial_options[:confirm]
|
263
|
+
agent_call.on_answer { ignoring_ended_calls { agent_call.join queued_call.uri if queued_call.active? } }
|
264
|
+
end
|
265
|
+
|
266
|
+
# Disconnect agent if caller hangs up before agent answers
|
267
|
+
queued_call.on_end { ignoring_ended_calls { agent_call.hangup } }
|
268
|
+
|
269
|
+
agent_call.on_unjoined do
|
270
|
+
ignoring_ended_calls { agent_call.hangup }
|
271
|
+
ignoring_ended_calls { queued_call.hangup }
|
272
|
+
end
|
273
|
+
|
274
|
+
# Track whether the agent actually talks to the queued_call
|
275
|
+
connected = false
|
276
|
+
queued_call.on_joined { connected = true }
|
277
|
+
|
278
|
+
agent_call.on_end do |end_event|
|
279
|
+
# Ensure we don't return an agent that was removed or paused
|
280
|
+
conditionally_return_agent agent
|
281
|
+
|
282
|
+
agent.callback :disconnect, self, agent_call, queued_call
|
283
|
+
|
284
|
+
unless connected
|
285
|
+
if queued_call.alive? && queued_call.active?
|
286
|
+
ignoring_ended_calls { priority_enqueue queued_call }
|
287
|
+
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
|
+
else
|
289
|
+
logger.warn "Caller #{queued_caller_id} hung up before being connected to an agent."
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
agent.callback :connect, self, agent_call, queued_call
|
295
|
+
|
296
|
+
agent_call.execute_controller_or_router_on_answer dial_options.delete(:confirm), dial_options.delete(:confirm_metadata)
|
297
|
+
|
298
|
+
agent_call.dial agent.address, dial_options
|
299
|
+
end
|
300
|
+
|
301
|
+
def bridge_agent(agent, queued_call)
|
302
|
+
# Stash caller ID to make log messages work even if calls end
|
303
|
+
queued_caller_id = remote_party queued_call
|
304
|
+
agent.call[:queued_call] = queued_call
|
305
|
+
|
306
|
+
agent.call.register_tmp_handler :event, Punchblock::Event::Unjoined do
|
307
|
+
agent.callback :disconnect, self, agent.call, queued_call
|
308
|
+
ignoring_ended_calls { queued_call.hangup }
|
309
|
+
ignoring_ended_calls { conditionally_return_agent agent if agent.call.active? }
|
310
|
+
agent.call[:queued_call] = nil
|
311
|
+
end
|
312
|
+
|
313
|
+
agent.callback :connect, self, agent.call, queued_call
|
314
|
+
|
315
|
+
agent.join queued_call if queued_call.active?
|
316
|
+
rescue *ENDED_CALL_EXCEPTIONS
|
317
|
+
ignoring_ended_calls do
|
318
|
+
if agent.call.active?
|
319
|
+
logger.info "Caller #{queued_caller_id} failed to connect to Agent #{agent.id} due to caller hangup"
|
320
|
+
conditionally_return_agent agent, :auto
|
321
|
+
else
|
322
|
+
# Agent's call has ended, so remove him from the queue
|
323
|
+
remove_agent agent
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
ignoring_ended_calls do
|
328
|
+
if queued_call.active?
|
329
|
+
priority_enqueue queued_call
|
330
|
+
logger.warn "Call failed to connect to Agent #{agent.id} due to agent hangup; reinserting caller #{queued_caller_id} into queue"
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'electric_slide/agent'
|
4
|
+
|
5
|
+
describe ElectricSlide::Agent do
|
6
|
+
let(:options) { { id: 1, address: '123@foo.com', presence: :available} }
|
7
|
+
|
8
|
+
class MyAgent < ElectricSlide::Agent
|
9
|
+
on_connect do
|
10
|
+
foo
|
11
|
+
end
|
12
|
+
|
13
|
+
def foo
|
14
|
+
:bar
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
subject {MyAgent.new options}
|
19
|
+
|
20
|
+
it 'executes a connect callback' do
|
21
|
+
expect(subject.callback(:connect)).to eql :bar
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'electric_slide/agent_strategy/fixed_priority'
|
5
|
+
require 'ostruct'
|
6
|
+
|
7
|
+
describe ElectricSlide::AgentStrategy::FixedPriority do
|
8
|
+
let(:subject) { ElectricSlide::AgentStrategy::FixedPriority.new }
|
9
|
+
it 'should allow adding an agent with a specified priority' do
|
10
|
+
subject.agent_available?.should be false
|
11
|
+
subject << OpenStruct.new({ id: 101, priority: 1 })
|
12
|
+
subject.agent_available?.should be true
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should allow adding multiple agents at the same priority' do
|
16
|
+
agent1 = OpenStruct.new({ id: 101, priority: 2 })
|
17
|
+
agent2 = OpenStruct.new({ id: 102, priority: 2 })
|
18
|
+
subject << agent1
|
19
|
+
subject << agent2
|
20
|
+
subject.checkout_agent.should == agent1
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should return all agents of a higher priority before returning an agent of a lower priority' do
|
24
|
+
agent1 = OpenStruct.new({ id: 101, priority: 2 })
|
25
|
+
agent2 = OpenStruct.new({ id: 102, priority: 2 })
|
26
|
+
agent3 = OpenStruct.new({ id: 103, priority: 3 })
|
27
|
+
subject << agent1
|
28
|
+
subject << agent2
|
29
|
+
subject << agent3
|
30
|
+
subject.checkout_agent.should == agent1
|
31
|
+
subject.checkout_agent.should == agent2
|
32
|
+
subject.checkout_agent.should == agent3
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should detect an agent available if one is available at any priority' do
|
36
|
+
agent1 = OpenStruct.new({ id: 101, priority: 2 })
|
37
|
+
agent2 = OpenStruct.new({ id: 102, priority: 3 })
|
38
|
+
subject << agent1
|
39
|
+
subject << agent2
|
40
|
+
subject.checkout_agent
|
41
|
+
subject.agent_available?.should == true
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe ElectricSlide::CallQueue do
|
5
|
+
let(:queue) { ElectricSlide::CallQueue.new }
|
6
|
+
let(:call_a) { dummy_call }
|
7
|
+
let(:call_b) { dummy_call }
|
8
|
+
let(:call_c) { dummy_call }
|
9
|
+
before :each do
|
10
|
+
queue.enqueue call_a
|
11
|
+
queue.enqueue call_b
|
12
|
+
queue.enqueue call_c
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should return callers in the same order they were enqueued" do
|
16
|
+
expect(queue.get_next_caller).to be call_a
|
17
|
+
expect(queue.get_next_caller).to be call_b
|
18
|
+
expect(queue.get_next_caller).to be call_c
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should return a priority caller ahead of the queue" do
|
22
|
+
call_d = dummy_call
|
23
|
+
queue.priority_enqueue call_d
|
24
|
+
expect(queue.get_next_caller).to be call_d
|
25
|
+
expect(queue.get_next_caller).to be call_a
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should remove a caller who abandons the queue" do
|
29
|
+
queue.enqueue call_a
|
30
|
+
queue.enqueue call_b
|
31
|
+
queue.abandon call_a
|
32
|
+
expect(queue.get_next_caller).to be call_b
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should raise when given an invalid connection type" do
|
36
|
+
expect { ElectricSlide::CallQueue.new connection_type: :blah }.to raise_error
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should raise when given an invalid Agent" do
|
40
|
+
expect { queue.add_agent nil }.to raise_error
|
41
|
+
end
|
42
|
+
end
|