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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6b6b7652ebb0629af9310a93d7c6ac0352fd92c8
4
- data.tar.gz: f5fd82735d7d8949b14a4ea8ee318c096820092f
3
+ metadata.gz: 7b7f9f14186f1dee1e5d75e38e79f6c1c127522e
4
+ data.tar.gz: 4f44af8a5750560e6f802227e36227142fba373f
5
5
  SHA512:
6
- metadata.gz: 9b9bafc665557361a51d504d71d94d2775a8e68924a22600e23a68f8b53acb7a2fcb85a3197e797e0c8af7cccc272f606d63f651c9a654f0aa9e2dc2aa69ac7a
7
- data.tar.gz: 85da4abe01db3cd099748cc11ada5c59383c38b8ac1e1994adaa01f98d9d6af05db659e767e6dae6cdef3c4fbae42c414689d9af2d09a670142b1355ec820caa
6
+ metadata.gz: 7d3f04f5f1ce98f47913ba04fed0da9cf827c9e1bed80e9e6731e0e22e371b6214e1e03550213a7f48abcf3259732b538ea75a71b8d65fc436311cd240d2747f
7
+ data.tar.gz: 6cf73dd16a211f07e738e0cbd779e1a099e88786e57ec087763cfa58c1b77a84917c2e39020354fad302ee4a11de36a0907d002925da38a1276b9a571aafdd4a
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
- source :rubygems
1
+ source 'https://rubygems.org'
2
2
  gemspec
3
3
 
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ group 'rspec' do
4
+ guard 'rspec', cmd: 'bundle exec rspec' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec/" }
8
+ end
9
+ end
data/README.markdown CHANGED
@@ -1,3 +1,153 @@
1
- AhnQueue - Automatic Call Distribution (ACD) Services for Adhearsion
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
@@ -10,7 +10,6 @@ require 'bundler/setup'
10
10
  require 'rspec/core/rake_task'
11
11
  RSpec::Core::RakeTask.new
12
12
 
13
- require 'ci/reporter/rake/rspec'
14
- task :ci => ['ci:setup:rspec', :spec]
15
- task :default => :spec
13
+ task ci: ['ci:setup:rspec', :spec]
14
+ task default: :spec
16
15
 
@@ -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 = "0.0.2"
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
 
@@ -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
- class ElectricSlide < Adhearsion::Plugin
6
- extend ActiveSupport::Autoload
5
+ require 'adhearsion/version'
7
6
 
8
- autoload :QueueStrategy
9
- autoload :RoundRobin
10
- autoload :RoundRobinMeetme
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, queue_type, agent_type = Agent)
19
- synchronize do
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
- def get_queue(name)
26
- synchronize do
27
- @queues[name]
28
- end
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 self.method_missing(method, *args, &block)
32
- instance.send method, *args, &block
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
- module Agent
36
- def work(agent_call)
37
- loop do
38
- agent_call.execute 'Bridge', @queue.next_call
39
- end
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
- class CalloutAgent
44
- def work(agent_channel)
45
- @queue.next_call.each do |next_call|
46
- next_call.dial agent_channel
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
- class MeetMeAgent
52
- include Agent
53
-
54
- def work(agent_call)
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
+