electric_slide 0.0.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b7f9f14186f1dee1e5d75e38e79f6c1c127522e
|
4
|
+
data.tar.gz: 4f44af8a5750560e6f802227e36227142fba373f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d3f04f5f1ce98f47913ba04fed0da9cf827c9e1bed80e9e6731e0e22e371b6214e1e03550213a7f48abcf3259732b538ea75a71b8d65fc436311cd240d2747f
|
7
|
+
data.tar.gz: 6cf73dd16a211f07e738e0cbd779e1a099e88786e57ec087763cfa58c1b77a84917c2e39020354fad302ee4a11de36a0907d002925da38a1276b9a571aafdd4a
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
CHANGED
data/Guardfile
ADDED
data/README.markdown
CHANGED
@@ -1,3 +1,153 @@
|
|
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.
|
5
|
+
|
6
|
+
To ensure proper operation, a few things are assumed:
|
7
|
+
|
8
|
+
* Agents will only be logged into a single queue at a time
|
9
|
+
If you have two types of agents (say "support" and "sales") then you should have two queues, each with their own pool of agents
|
10
|
+
* Agent authentication will happen before entering the queue - it is not the queue's concern
|
11
|
+
* The strategy for callers is FIFO: the caller who has been waiting the longest is the next to get an agent
|
12
|
+
* Queues will be implemented as a Celluloid Actor, which should protect the call selection strategies against race conditions
|
13
|
+
* There are two ways to connect an agent:
|
14
|
+
- If the Agent object provides an `address` attribute, and the queue's `connection_type` is set to `call`, then the queue will call the agent when a caller is waiting
|
15
|
+
- If the Agent object provides a `call` attribute, and the queue's `connection_type` is set to `bridge`, then the call queue will bridge the agent to the caller. In this mode, the agent hanging up will log him out of the queue
|
16
|
+
|
17
|
+
TODO:
|
18
|
+
* Example for using Matrioska to offer Agents and Callers interactivity while waiting
|
19
|
+
* How to handle MOH
|
20
|
+
|
21
|
+
## WARNING!
|
22
|
+
|
23
|
+
While you can have ElectricSlide keep track of custom queues, it is recommended to use the built-in CallQueue object.
|
24
|
+
|
25
|
+
The authors of ElectricSlide recommend NOT to subclass, monkeypatch, or otherwise alter the CallQueue implementation, as the likelihood of creating subtle race conditions is high.
|
26
|
+
|
27
|
+
Example Queue
|
28
|
+
-------------
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
my_queue = ElectricSlide.create :my_queue, ElectricSlide::CallQueue
|
32
|
+
|
33
|
+
# Another way to get a handle on a queue
|
34
|
+
ElectricSlide.create :my_queue
|
35
|
+
my_queue = ElectricSlide.get_queue :my_queue
|
36
|
+
```
|
37
|
+
|
38
|
+
|
39
|
+
Example CallController for Queued Call
|
40
|
+
--------------------------------------
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
class EnterTheQueue < Adhearsion::CallController
|
44
|
+
def run
|
45
|
+
answer
|
46
|
+
|
47
|
+
# Play music-on-hold to the caller until joined to an agent
|
48
|
+
player = play 'http://moh-server.example.com/stream.mp3', repeat_times: 0
|
49
|
+
call.on_joined do
|
50
|
+
player.stop!
|
51
|
+
end
|
52
|
+
|
53
|
+
ElectricSlide.get_queue(:my_queue).enqueue call
|
54
|
+
|
55
|
+
# The controller will exit, but the call will remain up
|
56
|
+
# The call will automatically hang up after speaking to an agent
|
57
|
+
call.auto_hangup = false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
|
63
|
+
Adding an Agent to the Queue
|
64
|
+
----------------------------
|
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.
|
67
|
+
|
68
|
+
To add an agent who will receive calls whenever a call is enqueued, do something like this:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
agent = ElectricSlide::Agent.new id: 1, address: 'sip:agent1@example.com', presence: :available
|
72
|
+
ElectricSlide.get_queue(:my_queue).add_agent agent
|
73
|
+
```
|
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!/
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
ElectricSlide.update_agent 1, presence: offline
|
79
|
+
```
|
80
|
+
|
81
|
+
If it is more convenient, you may also pass `#update_agent` an Agent-like object:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
options = {
|
85
|
+
id: 1,
|
86
|
+
address: 'sip:agent1@example.com',
|
87
|
+
presence: offline
|
88
|
+
}
|
89
|
+
agent = ElectricSlide::Agent.new options
|
90
|
+
ElectricSlide.update_agent 1, agent
|
91
|
+
```
|
92
|
+
|
93
|
+
Switching connection types
|
94
|
+
--------------------------
|
95
|
+
|
96
|
+
ElectricSlide provides two methods for connecting callers to agents:
|
97
|
+
- `:call`: (default) If the Agent object provides an `address` attribute, and the queue's `connection_type` is set to `call`, then the queue will call the agent when a caller is waiting
|
98
|
+
- `:bridge`: If the Agent object provides a `call` attribute, and the queue's `connection_type` is set to `bridge`, then the call queue will bridge the agent to the caller. In this mode, the agent hanging up will log him out of the queue
|
99
|
+
|
100
|
+
To select the connection type, specify it when creating the queue:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
ElectricSlide.create_queue :my_queue, ElectricSlide::CallQueue, connection_type: :bridge
|
104
|
+
```
|
105
|
+
|
106
|
+
Selecting an Agent distribution strategy
|
107
|
+
----------------------------------------
|
108
|
+
|
109
|
+
Different use-cases have different requirements for selecting the next agent to take a call. ElectricSlide provides two strategies which may be used. You are also welcome to create your own distribution strategy by implementing the same interface as described in `ElectricSlide::AgentStrategy::LongestIdle`.
|
110
|
+
|
111
|
+
To select an agent strategy, specify it when creating the queue:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
ElectricSlide.create_queue :my_queue, ElectricSlide::CallQueue, agent_strategy: ElectricSlide::AgentStrategy::LongestIdle
|
115
|
+
```
|
116
|
+
|
117
|
+
Two strategies are provided out-of-the-box:
|
118
|
+
|
119
|
+
* `ElectricSlide::AgentStrategy::LongestIdle` selects the agent that has been idle for the longest amount of time.
|
120
|
+
* `ElectricSlide::AgentStrategy::FixedPriority` selects the agent with the lowest numeric priority first. In the event that more than one agent is available at a given priority, then the agent that has been idle the longest at the lowest numeric priority is selected.
|
121
|
+
|
122
|
+
Custom Agent Behavior
|
123
|
+
----------------------------
|
124
|
+
|
125
|
+
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
|
+
|
127
|
+
* `on_connect`
|
128
|
+
* `on_disconnect`
|
129
|
+
|
130
|
+
Confirmation Controllers
|
131
|
+
------------------------
|
132
|
+
|
133
|
+
In case you need to execute a confirmation controller on the call that is placed to the agent, such as "Press 1 to accept the call", you currently need to pass in the confirmation class name and the call object as metadata in the `call_options_for` callback in your `ElectricSlide::Agent` subclass.
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
# an example from the Agent subclass
|
137
|
+
def dial_options_for(queue, queued_call)
|
138
|
+
{
|
139
|
+
from: caller_digits(queued_call.from),
|
140
|
+
timeout: on_pstn? ? APP_CONFIG.agent_timeout * 3 : APP_CONFIG.agent_timeout,
|
141
|
+
confirm: MyConfirmationController,
|
142
|
+
confirm_metadata: {caller: queued_call, agent: self},
|
143
|
+
}
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
You then need to handle the join in your confirmation controller, using for example:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
call.join metadata[:caller] if confirm!
|
151
|
+
```
|
152
|
+
|
153
|
+
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.
|
data/Rakefile
CHANGED
data/electric_slide.gemspec
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require 'electric_slide/version'
|
1
4
|
require 'date'
|
2
5
|
|
3
6
|
Gem::Specification.new do |s|
|
4
7
|
s.name = "electric_slide"
|
5
|
-
s.version =
|
8
|
+
s.version = ElectricSlide::VERSION
|
6
9
|
|
7
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
11
|
s.authors = ["Ben Klang"]
|
@@ -23,8 +26,9 @@ Gem::Specification.new do |s|
|
|
23
26
|
s.add_runtime_dependency 'countdownlatch'
|
24
27
|
s.add_runtime_dependency 'activesupport'
|
25
28
|
s.add_development_dependency 'rspec', ['>= 2.5.0']
|
26
|
-
s.add_development_dependency 'flexmock', ['>= 0.9.0']
|
27
29
|
s.add_development_dependency 'ci_reporter'
|
30
|
+
s.add_development_dependency 'guard'
|
31
|
+
s.add_development_dependency 'guard-rspec'
|
28
32
|
s.add_development_dependency 'simplecov'
|
29
33
|
s.add_development_dependency 'simplecov-rcov'
|
30
34
|
|
data/lib/electric_slide.rb
CHANGED
@@ -1,65 +1,68 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'celluloid'
|
1
3
|
require 'singleton'
|
2
|
-
require 'active_support/dependencies/autoload'
|
3
|
-
require 'adhearsion/foundation/thread_safety'
|
4
4
|
|
5
|
-
|
6
|
-
extend ActiveSupport::Autoload
|
5
|
+
require 'adhearsion/version'
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
if Gem::Version.new(Adhearsion::VERSION) < Gem::Version.new('3.0.0')
|
8
|
+
# Backport https://github.com/adhearsion/adhearsion/commit/8c6855612c70dd822fb4e4c2006d1fdc9d05fe23 to avoid confusion around dead calls
|
9
|
+
require 'adhearsion/call'
|
10
|
+
class Adhearsion::Call::ActorProxy < Celluloid::ActorProxy
|
11
|
+
def active?
|
12
|
+
alive? && super
|
13
|
+
rescue Adhearsion::Call::ExpiredError
|
14
|
+
false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
%w(
|
20
|
+
call_queue
|
21
|
+
plugin
|
22
|
+
).each { |f| require "electric_slide/#{f}" }
|
11
23
|
|
24
|
+
class ElectricSlide
|
12
25
|
include Singleton
|
13
26
|
|
14
27
|
def initialize
|
28
|
+
@mutex = Mutex.new
|
15
29
|
@queues = {}
|
16
30
|
end
|
17
31
|
|
18
|
-
def create(name,
|
19
|
-
|
20
|
-
@queues[name] = queue_type.new unless @queues.has_key?(name)
|
21
|
-
@queues[name].extend agent_type
|
22
|
-
end
|
23
|
-
end
|
32
|
+
def create(name, queue_class = nil, *args)
|
33
|
+
fail "Queue with name #{name} already exists!" if @queues.key? name
|
24
34
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
35
|
+
queue_class ||= CallQueue
|
36
|
+
@queues[name] = queue_class.work *args
|
37
|
+
# Return the queue instance or current actor
|
38
|
+
get_queue name
|
29
39
|
end
|
30
40
|
|
31
|
-
def
|
32
|
-
|
41
|
+
def get_queue!(name)
|
42
|
+
fail "Queue #{name} not found!" unless @queues.key?(name)
|
43
|
+
get_queue name
|
33
44
|
end
|
34
45
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
40
53
|
end
|
41
54
|
end
|
42
55
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
48
|
-
end
|
56
|
+
def shutdown_queue(name)
|
57
|
+
queue = get_queue name
|
58
|
+
queue.terminate
|
59
|
+
@queues.delete name
|
49
60
|
end
|
50
61
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
loop do
|
56
|
-
agent_call.join agent_conf, @queue.next_call
|
57
|
-
end
|
62
|
+
def self.method_missing(method, *args, &block)
|
63
|
+
@@mutex ||= Mutex.new
|
64
|
+
@@mutex.synchronize do
|
65
|
+
instance.send method, *args, &block
|
58
66
|
end
|
59
67
|
end
|
60
|
-
|
61
|
-
class BridgeAgent
|
62
|
-
include Agent
|
63
|
-
end
|
64
68
|
end
|
65
|
-
|
@@ -0,0 +1,48 @@
|
|
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
|
+
|
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
|
19
|
+
|
20
|
+
|
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
|
26
|
+
|
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
|
32
|
+
|
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
|
37
|
+
|
38
|
+
def join(queued_call)
|
39
|
+
# For use in queues that need bridge connections
|
40
|
+
@call.join queued_call
|
41
|
+
end
|
42
|
+
|
43
|
+
# FIXME: Use delegator?
|
44
|
+
def from
|
45
|
+
@call.from
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class ElectricSlide
|
4
|
+
class AgentStrategy
|
5
|
+
class FixedPriority
|
6
|
+
def initialize
|
7
|
+
@priorities = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def agent_available?
|
11
|
+
!!@priorities.detect do |priority, agents|
|
12
|
+
agents.present?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns information about the number of available agents
|
17
|
+
# The data returned depends on the AgentStrategy in use.
|
18
|
+
# @return [Hash] Summary information about agents available, depending on strategy
|
19
|
+
# :total: The total number of available agents
|
20
|
+
# :priorities: A Hash containing the number of available agents at each priority
|
21
|
+
def available_agent_summary
|
22
|
+
@priorities.inject({}) do |summary, data|
|
23
|
+
priority, agents = *data
|
24
|
+
summary[:total] ||= 0
|
25
|
+
summary[:total] += agents.count
|
26
|
+
summary[:priorities] ||= {}
|
27
|
+
summary[:priorities][priority] = agents.count
|
28
|
+
summary
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def checkout_agent
|
33
|
+
_, agents = @priorities.detect do |priority, agents|
|
34
|
+
agents.present?
|
35
|
+
end
|
36
|
+
agents.shift
|
37
|
+
end
|
38
|
+
|
39
|
+
def <<(agent)
|
40
|
+
# TODO: How aggressively do we check for agents duplicated in multiple priorities?
|
41
|
+
raise ArgumentError, "Agents must have a specified priority" unless agent.respond_to?(:priority)
|
42
|
+
priority = agent.priority || 999999
|
43
|
+
@priorities[priority] ||= []
|
44
|
+
@priorities[priority] << agent unless @priorities[priority].include? agent
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete(agent)
|
48
|
+
@priorities.detect do |priority, agents|
|
49
|
+
agents.delete(agent)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class ElectricSlide
|
4
|
+
class AgentStrategy
|
5
|
+
class LongestIdle
|
6
|
+
def initialize
|
7
|
+
@free_agents = [] # Needed to keep track of waiting order
|
8
|
+
end
|
9
|
+
|
10
|
+
# Checks whether an agent is available to take a call
|
11
|
+
# @return [Boolean] True if an agent is available
|
12
|
+
def agent_available?
|
13
|
+
@free_agents.count > 0
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns a count of the number of available agents
|
17
|
+
# @return [Hash] Hash of information about available agents
|
18
|
+
# This strategy only returns the count of agents available with :total
|
19
|
+
def available_agent_summary
|
20
|
+
{ total: @free_agents.count }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Assigns the first available agent, marking the agent :busy
|
24
|
+
# @return {Agent}
|
25
|
+
def checkout_agent
|
26
|
+
@free_agents.shift
|
27
|
+
end
|
28
|
+
|
29
|
+
def <<(agent)
|
30
|
+
@free_agents << agent unless @free_agents.include?(agent)
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete(agent)
|
34
|
+
@free_agents.delete(agent)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|